deepctl-core 0.2.0__tar.gz → 0.2.2__tar.gz

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 (23) hide show
  1. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/PKG-INFO +1 -1
  2. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/pyproject.toml +1 -1
  3. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core/__init__.py +2 -0
  4. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core/base_command.py +5 -1
  5. deepctl_core-0.2.2/src/deepctl_core/client.py +542 -0
  6. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core/output.py +60 -33
  7. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core/skill_generator.py +53 -0
  8. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core.egg-info/PKG-INFO +1 -1
  9. deepctl_core-0.2.0/src/deepctl_core/client.py +0 -305
  10. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/README.md +0 -0
  11. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/setup.cfg +0 -0
  12. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core/auth.py +0 -0
  13. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core/base_group_command.py +0 -0
  14. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core/config.py +0 -0
  15. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core/models.py +0 -0
  16. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core/plugin_env.py +0 -0
  17. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core/plugin_manager.py +0 -0
  18. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core/py.typed +0 -0
  19. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core/timing.py +0 -0
  20. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core.egg-info/SOURCES.txt +0 -0
  21. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core.egg-info/dependency_links.txt +0 -0
  22. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core.egg-info/requires.txt +0 -0
  23. {deepctl_core-0.2.0 → deepctl_core-0.2.2}/src/deepctl_core.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepctl-core
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Core components for deepctl
5
5
  Author-email: Deepgram <devrel@deepgram.com>
6
6
  Maintainer-email: Deepgram <devrel@deepgram.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepctl-core"
7
- version = "0.2.0" # x-release-please-version
7
+ version = "0.2.2" # x-release-please-version
8
8
  description = "Core components for deepctl"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -16,6 +16,7 @@ from .output import (
16
16
  OutputFormatter,
17
17
  get_console,
18
18
  get_output_format,
19
+ is_agentic,
19
20
  print_error,
20
21
  print_info,
21
22
  print_output,
@@ -50,6 +51,7 @@ __all__ = [
50
51
  "enable_timing",
51
52
  "get_console",
52
53
  "get_timing_summary",
54
+ "is_agentic",
53
55
  "is_timing_enabled",
54
56
  "print_error",
55
57
  "print_info",
@@ -189,7 +189,11 @@ class BaseCommand(ABC):
189
189
  result = [item.model_dump() for item in result]
190
190
 
191
191
  with TimingContext(f"output_{output_format}"):
192
- if output_format in ("json", "default"):
192
+ if output_format == "default":
193
+ # Commands handle their own display in default mode;
194
+ # only emit structured output when explicitly requested.
195
+ return
196
+ elif output_format == "json":
193
197
  self._output_json(result)
194
198
  elif output_format == "yaml":
195
199
  self._output_yaml(result)
@@ -0,0 +1,542 @@
1
+ """Deepgram SDK wrapper for deepctl with authentication integration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any, cast
7
+
8
+ from deepgram import DeepgramClient as DGClient
9
+ from deepgram import DeepgramClientEnvironment
10
+ from deepgram.core.api_error import ApiError
11
+ from rich.console import Console
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Iterator
15
+ from datetime import datetime
16
+
17
+ from .auth import AuthManager
18
+ from .config import Config
19
+
20
+ console = Console()
21
+
22
+
23
+ class DeepgramClient:
24
+ """Wrapper around Deepgram SDK with authentication integration."""
25
+
26
+ def __init__(self, config: Config, auth_manager: AuthManager):
27
+ self.config = config
28
+ self.auth_manager = auth_manager
29
+ self._client: DGClient | None = None
30
+ self._project_id: str | None = None
31
+
32
+ @property
33
+ def client(self) -> DGClient:
34
+ """Get authenticated Deepgram client."""
35
+ if self._client is None:
36
+ self._client = self._create_client()
37
+ return self._client
38
+
39
+ def _resolve_project_id(self, project_id: str | None = None) -> str:
40
+ """Resolve project ID from argument, config, or auth manager."""
41
+ if project_id:
42
+ return project_id
43
+ _ = self.client # ensure client is initialized
44
+ resolved = self._project_id or self.auth_manager.get_project_id()
45
+ if not resolved:
46
+ raise ApiError(body="No project ID available")
47
+ return resolved
48
+
49
+ def _create_client(self) -> DGClient:
50
+ """Create authenticated Deepgram client."""
51
+ self.auth_manager.guard()
52
+ api_key = self.auth_manager.get_api_key()
53
+ project_id = self.auth_manager.get_project_id()
54
+
55
+ if not api_key:
56
+ raise ApiError(body="No API key available")
57
+
58
+ current_profile = self.config.get_profile()
59
+
60
+ try:
61
+ kwargs: dict[str, Any] = {"api_key": api_key}
62
+
63
+ if (
64
+ current_profile.base_url
65
+ and current_profile.base_url != "https://api.deepgram.com"
66
+ ):
67
+ kwargs["environment"] = DeepgramClientEnvironment(
68
+ base=current_profile.base_url,
69
+ production=current_profile.base_url,
70
+ agent=current_profile.base_url,
71
+ )
72
+
73
+ client = DGClient(**kwargs)
74
+ self._project_id = project_id
75
+ return client
76
+
77
+ except Exception as e:
78
+ console.print(f"[red]Error creating Deepgram client:[/red] {e}")
79
+ raise ApiError(body=f"Failed to create client: {e}")
80
+
81
+ # ── Speech-to-Text (pre-recorded) ──────────────────────────────
82
+
83
+ def transcribe_file(
84
+ self,
85
+ file_path: str | Path,
86
+ options: dict[str, Any] | None = None,
87
+ ) -> dict[str, Any]:
88
+ """Transcribe an audio file."""
89
+ file_path = Path(file_path)
90
+ if not file_path.exists():
91
+ raise FileNotFoundError(f"Audio file not found: {file_path}")
92
+
93
+ default_options: dict[str, Any] = {
94
+ "model": "nova-3",
95
+ "smart_format": True,
96
+ "language": "en-US",
97
+ }
98
+ if options:
99
+ default_options.update(options)
100
+
101
+ try:
102
+ with open(file_path, "rb") as audio_file:
103
+ audio_data = audio_file.read()
104
+
105
+ response = self.client.listen.v1.media.transcribe_file(
106
+ request=audio_data, **default_options
107
+ )
108
+ return cast("dict[str, Any]", response.model_dump())
109
+
110
+ except Exception as e:
111
+ raise ApiError(body=f"Transcription failed: {e}")
112
+
113
+ def transcribe_url(
114
+ self, url: str, options: dict[str, Any] | None = None
115
+ ) -> dict[str, Any]:
116
+ """Transcribe audio from URL."""
117
+ default_options: dict[str, Any] = {
118
+ "model": "nova-3",
119
+ "smart_format": True,
120
+ "language": "en-US",
121
+ }
122
+ if options:
123
+ default_options.update(options)
124
+
125
+ try:
126
+ response = self.client.listen.v1.media.transcribe_url(
127
+ url=url, **default_options
128
+ )
129
+ return cast("dict[str, Any]", response.model_dump())
130
+
131
+ except Exception as e:
132
+ raise ApiError(body=f"Transcription failed: {e}")
133
+
134
+ # ── Text-to-Speech ─────────────────────────────────────────────
135
+
136
+ def speak_text(
137
+ self,
138
+ text: str,
139
+ model: str = "aura-2-asteria-en",
140
+ encoding: str | None = None,
141
+ container: str | None = None,
142
+ sample_rate: float | None = None,
143
+ ) -> Iterator[bytes]:
144
+ """Generate speech audio from text. Returns an iterator of bytes."""
145
+ try:
146
+ kwargs: dict[str, Any] = {"text": text, "model": model}
147
+ if encoding:
148
+ kwargs["encoding"] = encoding
149
+ if container:
150
+ kwargs["container"] = container
151
+ if sample_rate:
152
+ kwargs["sample_rate"] = sample_rate
153
+
154
+ return cast(
155
+ "Iterator[bytes]", self.client.speak.v1.audio.generate(**kwargs)
156
+ )
157
+
158
+ except Exception as e:
159
+ raise ApiError(body=f"Text-to-speech failed: {e}")
160
+
161
+ # ── Text Intelligence (Read) ───────────────────────────────────
162
+
163
+ def analyze_text(
164
+ self,
165
+ text: str,
166
+ sentiment: bool = False,
167
+ summarize: bool = False,
168
+ topics: bool = False,
169
+ intents: bool = False,
170
+ language: str | None = None,
171
+ ) -> dict[str, Any]:
172
+ """Analyze text for sentiment, topics, intents, summary."""
173
+ try:
174
+ from deepgram.requests.read_v1request_text import ReadV1RequestTextParams
175
+
176
+ kwargs: dict[str, Any] = {
177
+ "request": ReadV1RequestTextParams(text=text),
178
+ }
179
+ if sentiment:
180
+ kwargs["sentiment"] = True
181
+ if summarize:
182
+ kwargs["summarize"] = "v2"
183
+ if topics:
184
+ kwargs["topics"] = True
185
+ if intents:
186
+ kwargs["intents"] = True
187
+ kwargs["language"] = language or "en"
188
+
189
+ response = self.client.read.v1.text.analyze(**kwargs)
190
+ return cast("dict[str, Any]", response.model_dump())
191
+
192
+ except Exception as e:
193
+ raise ApiError(body=f"Text analysis failed: {e}")
194
+
195
+ # ── Models ─────────────────────────────────────────────────────
196
+
197
+ def list_models(self, include_outdated: bool = False) -> dict[str, Any]:
198
+ """List all public models."""
199
+ try:
200
+ kwargs: dict[str, Any] = {}
201
+ if include_outdated:
202
+ kwargs["include_outdated"] = True
203
+
204
+ response = self.client.manage.v1.models.list(**kwargs)
205
+ return cast("dict[str, Any]", response.model_dump())
206
+
207
+ except Exception as e:
208
+ raise ApiError(body=f"Failed to list models: {e}")
209
+
210
+ def get_model(self, model_id: str) -> dict[str, Any]:
211
+ """Get a specific model by ID."""
212
+ try:
213
+ response = self.client.manage.v1.models.get(model_id)
214
+ return cast("dict[str, Any]", response.model_dump())
215
+
216
+ except Exception as e:
217
+ raise ApiError(body=f"Failed to get model: {e}")
218
+
219
+ # ── Projects ───────────────────────────────────────────────────
220
+
221
+ def get_projects(self) -> dict[str, Any]:
222
+ """Get user's projects."""
223
+ try:
224
+ response = self.client.manage.v1.projects.list()
225
+ return cast("dict[str, Any]", response.model_dump())
226
+
227
+ except Exception as e:
228
+ raise ApiError(body=f"Failed to get projects: {e}")
229
+
230
+ def get_project(self, project_id: str | None = None) -> dict[str, Any]:
231
+ """Get specific project."""
232
+ pid = self._resolve_project_id(project_id)
233
+ try:
234
+ response = self.client.manage.v1.projects.get(pid)
235
+ return cast("dict[str, Any]", response.model_dump())
236
+
237
+ except Exception as e:
238
+ raise ApiError(body=f"Failed to get project: {e}")
239
+
240
+ def create_project(self, name: str, company: str | None = None) -> dict[str, Any]:
241
+ """Create a new project."""
242
+ try:
243
+ project_data: dict[str, str] = {"name": name}
244
+ if company:
245
+ project_data["company"] = company
246
+
247
+ response = self.client.manage.v1.projects.update(request=project_data)
248
+ return cast("dict[str, Any]", response.model_dump())
249
+
250
+ except Exception as e:
251
+ raise ApiError(body=f"Failed to create project: {e}")
252
+
253
+ # ── Usage ──────────────────────────────────────────────────────
254
+
255
+ def get_usage(
256
+ self,
257
+ project_id: str | None = None,
258
+ start_date: str | None = None,
259
+ end_date: str | None = None,
260
+ ) -> dict[str, Any]:
261
+ """Get usage statistics."""
262
+ pid = self._resolve_project_id(project_id)
263
+ try:
264
+ kwargs: dict[str, Any] = {}
265
+ if start_date:
266
+ kwargs["start"] = start_date
267
+ if end_date:
268
+ kwargs["end"] = end_date
269
+
270
+ response = self.client.manage.v1.projects.usage.get(pid, **kwargs)
271
+ result = cast("dict[str, Any]", response.model_dump())
272
+ result["project_id"] = pid
273
+ return result
274
+
275
+ except Exception as e:
276
+ raise ApiError(body=f"Failed to get usage: {e}")
277
+
278
+ # ── API Keys ───────────────────────────────────────────────────
279
+
280
+ def list_keys(
281
+ self,
282
+ project_id: str | None = None,
283
+ status: str | None = None,
284
+ ) -> dict[str, Any]:
285
+ """List API keys for a project."""
286
+ pid = self._resolve_project_id(project_id)
287
+ try:
288
+ kwargs: dict[str, Any] = {}
289
+ if status:
290
+ kwargs["status"] = status
291
+
292
+ response = self.client.manage.v1.projects.keys.list(pid, **kwargs)
293
+ return cast("dict[str, Any]", response.model_dump())
294
+
295
+ except Exception as e:
296
+ raise ApiError(body=f"Failed to list keys: {e}")
297
+
298
+ def create_key(
299
+ self,
300
+ project_id: str | None = None,
301
+ comment: str = "",
302
+ scopes: list[str] | None = None,
303
+ expiration_date: str | None = None,
304
+ time_to_live: int | None = None,
305
+ tags: list[str] | None = None,
306
+ ) -> dict[str, Any]:
307
+ """Create a new API key."""
308
+ pid = self._resolve_project_id(project_id)
309
+ try:
310
+ request_data: dict[str, Any] = {
311
+ "comment": comment,
312
+ "scopes": scopes or ["member"],
313
+ }
314
+ if expiration_date:
315
+ request_data["expiration_date"] = expiration_date
316
+ if time_to_live:
317
+ request_data["time_to_live_in_seconds"] = time_to_live
318
+ if tags:
319
+ request_data["tags"] = tags
320
+
321
+ response = self.client.manage.v1.projects.keys.create(
322
+ pid, request=request_data
323
+ )
324
+ return cast("dict[str, Any]", response.model_dump())
325
+
326
+ except Exception as e:
327
+ raise ApiError(body=f"Failed to create key: {e}")
328
+
329
+ def get_key(self, key_id: str, project_id: str | None = None) -> dict[str, Any]:
330
+ """Get a specific API key."""
331
+ pid = self._resolve_project_id(project_id)
332
+ try:
333
+ response = self.client.manage.v1.projects.keys.get(pid, key_id)
334
+ return cast("dict[str, Any]", response.model_dump())
335
+
336
+ except Exception as e:
337
+ raise ApiError(body=f"Failed to get key: {e}")
338
+
339
+ def delete_key(self, key_id: str, project_id: str | None = None) -> dict[str, Any]:
340
+ """Delete an API key."""
341
+ pid = self._resolve_project_id(project_id)
342
+ try:
343
+ response = self.client.manage.v1.projects.keys.delete(pid, key_id)
344
+ return cast("dict[str, Any]", response.model_dump())
345
+
346
+ except Exception as e:
347
+ raise ApiError(body=f"Failed to delete key: {e}")
348
+
349
+ # ── Requests ───────────────────────────────────────────────────
350
+
351
+ def list_requests(
352
+ self,
353
+ project_id: str | None = None,
354
+ start: datetime | None = None,
355
+ end: datetime | None = None,
356
+ limit: int | None = None,
357
+ page: int | None = None,
358
+ status: str | None = None,
359
+ endpoint: str | None = None,
360
+ method: str | None = None,
361
+ ) -> dict[str, Any]:
362
+ """List API requests for a project."""
363
+ pid = self._resolve_project_id(project_id)
364
+ try:
365
+ kwargs: dict[str, Any] = {}
366
+ if start:
367
+ kwargs["start"] = start
368
+ if end:
369
+ kwargs["end"] = end
370
+ if limit:
371
+ kwargs["limit"] = limit
372
+ if page:
373
+ kwargs["page"] = page
374
+ if status:
375
+ kwargs["status"] = status
376
+ if endpoint:
377
+ kwargs["endpoint"] = endpoint
378
+ if method:
379
+ kwargs["method"] = method
380
+
381
+ response = self.client.manage.v1.projects.requests.list(pid, **kwargs)
382
+ return cast("dict[str, Any]", response.model_dump())
383
+
384
+ except Exception as e:
385
+ raise ApiError(body=f"Failed to list requests: {e}")
386
+
387
+ def get_request(
388
+ self, request_id: str, project_id: str | None = None
389
+ ) -> dict[str, Any]:
390
+ """Get a specific API request."""
391
+ pid = self._resolve_project_id(project_id)
392
+ try:
393
+ response = self.client.manage.v1.projects.requests.get(pid, request_id)
394
+ return cast("dict[str, Any]", response.model_dump())
395
+
396
+ except Exception as e:
397
+ raise ApiError(body=f"Failed to get request: {e}")
398
+
399
+ # ── Billing ────────────────────────────────────────────────────
400
+
401
+ def get_billing_breakdown(
402
+ self,
403
+ project_id: str | None = None,
404
+ start: str | None = None,
405
+ end: str | None = None,
406
+ grouping: str | None = None,
407
+ ) -> dict[str, Any]:
408
+ """Get billing breakdown for a project."""
409
+ pid = self._resolve_project_id(project_id)
410
+ try:
411
+ kwargs: dict[str, Any] = {}
412
+ if start:
413
+ kwargs["start"] = start
414
+ if end:
415
+ kwargs["end"] = end
416
+ if grouping:
417
+ kwargs["grouping"] = grouping
418
+
419
+ response = self.client.manage.v1.projects.billing.breakdown.list(
420
+ pid, **kwargs
421
+ )
422
+ return cast("dict[str, Any]", response.model_dump())
423
+
424
+ except Exception as e:
425
+ raise ApiError(body=f"Failed to get billing breakdown: {e}")
426
+
427
+ def get_balances(self, project_id: str | None = None) -> dict[str, Any]:
428
+ """Get project balances."""
429
+ pid = self._resolve_project_id(project_id)
430
+ try:
431
+ response = self.client.manage.v1.projects.billing.balances.list(pid)
432
+ return cast("dict[str, Any]", response.model_dump())
433
+
434
+ except Exception as e:
435
+ raise ApiError(body=f"Failed to get balances: {e}")
436
+
437
+ # ── Members ────────────────────────────────────────────────────
438
+
439
+ def list_members(self, project_id: str | None = None) -> dict[str, Any]:
440
+ """List project members."""
441
+ pid = self._resolve_project_id(project_id)
442
+ try:
443
+ response = self.client.manage.v1.projects.members.list(pid)
444
+ return cast("dict[str, Any]", response.model_dump())
445
+
446
+ except Exception as e:
447
+ raise ApiError(body=f"Failed to list members: {e}")
448
+
449
+ def remove_member(
450
+ self, member_id: str, project_id: str | None = None
451
+ ) -> dict[str, Any]:
452
+ """Remove a member from the project."""
453
+ pid = self._resolve_project_id(project_id)
454
+ try:
455
+ response = self.client.manage.v1.projects.members.delete(pid, member_id)
456
+ return cast("dict[str, Any]", response.model_dump())
457
+
458
+ except Exception as e:
459
+ raise ApiError(body=f"Failed to remove member: {e}")
460
+
461
+ def list_member_scopes(
462
+ self, member_id: str, project_id: str | None = None
463
+ ) -> dict[str, Any]:
464
+ """List scopes for a project member."""
465
+ pid = self._resolve_project_id(project_id)
466
+ try:
467
+ response = self.client.manage.v1.projects.members.scopes.list(
468
+ pid, member_id
469
+ )
470
+ return cast("dict[str, Any]", response.model_dump())
471
+
472
+ except Exception as e:
473
+ raise ApiError(body=f"Failed to list member scopes: {e}")
474
+
475
+ def update_member_scopes(
476
+ self, member_id: str, scope: str, project_id: str | None = None
477
+ ) -> dict[str, Any]:
478
+ """Update scopes for a project member."""
479
+ pid = self._resolve_project_id(project_id)
480
+ try:
481
+ response = self.client.manage.v1.projects.members.scopes.update(
482
+ pid, member_id, scope=scope
483
+ )
484
+ return cast("dict[str, Any]", response.model_dump())
485
+
486
+ except Exception as e:
487
+ raise ApiError(body=f"Failed to update member scopes: {e}")
488
+
489
+ # ── Invites ────────────────────────────────────────────────────
490
+
491
+ def list_invites(self, project_id: str | None = None) -> dict[str, Any]:
492
+ """List project invites."""
493
+ pid = self._resolve_project_id(project_id)
494
+ try:
495
+ response = self.client.manage.v1.projects.members.invites.list(pid)
496
+ return cast("dict[str, Any]", response.model_dump())
497
+
498
+ except Exception as e:
499
+ raise ApiError(body=f"Failed to list invites: {e}")
500
+
501
+ def create_invite(
502
+ self, email: str, scope: str, project_id: str | None = None
503
+ ) -> dict[str, Any]:
504
+ """Invite a member to the project."""
505
+ pid = self._resolve_project_id(project_id)
506
+ try:
507
+ response = self.client.manage.v1.projects.members.invites.create(
508
+ pid, email=email, scope=scope
509
+ )
510
+ return cast("dict[str, Any]", response.model_dump())
511
+
512
+ except Exception as e:
513
+ raise ApiError(body=f"Failed to create invite: {e}")
514
+
515
+ def delete_invite(
516
+ self, email: str, project_id: str | None = None
517
+ ) -> dict[str, Any]:
518
+ """Delete a project invite."""
519
+ pid = self._resolve_project_id(project_id)
520
+ try:
521
+ response = self.client.manage.v1.projects.members.invites.delete(pid, email)
522
+ return cast("dict[str, Any]", response.model_dump())
523
+
524
+ except Exception as e:
525
+ raise ApiError(body=f"Failed to delete invite: {e}")
526
+
527
+ # ── Utilities ──────────────────────────────────────────────────
528
+
529
+ def validate_api_key(self, api_key: str | None = None) -> bool:
530
+ """Validate API key by making a simple API call."""
531
+ project_id = self.auth_manager.get_project_id()
532
+ success, _, _ = self.auth_manager.verify_credentials(
533
+ api_key=api_key, project_id=project_id
534
+ )
535
+ return success
536
+
537
+ def test_connection(self) -> bool:
538
+ """Test connection to Deepgram API."""
539
+ success, message, _ = self.auth_manager.verify_credentials()
540
+ if not success:
541
+ console.print(f"[red]Connection test failed:[/red] {message}")
542
+ return success
@@ -1,6 +1,8 @@
1
1
  """Output formatting utilities for deepctl."""
2
2
 
3
3
  import json
4
+ import os
5
+ import sys
4
6
  from io import StringIO
5
7
  from typing import Any
6
8
 
@@ -18,16 +20,42 @@ from rich.progress import (
18
20
  from rich.syntax import Syntax
19
21
  from rich.table import Table
20
22
 
21
- # Global console instance
22
- console = Console()
23
- stderr_console = Console(stderr=True)
23
+ # ── Agentic / non-TTY detection ──────────────────────────────────
24
+
25
+ _AGENTIC_ENV_VARS = (
26
+ "CLAUDE_CODE",
27
+ "CODEX_CLI",
28
+ "GEMINI_CLI",
29
+ "AIDER",
30
+ "CONTINUE_GLOBAL_DIR",
31
+ )
32
+
33
+
34
+ def is_agentic() -> bool:
35
+ """Detect if running inside an AI coding tool or non-interactive pipe.
36
+
37
+ Returns True when stdout is not a terminal OR a known agentic
38
+ environment variable is set. This lets commands produce
39
+ machine-friendly output automatically.
40
+ """
41
+ if not sys.stdout.isatty():
42
+ return True
43
+ return any(os.environ.get(v) for v in _AGENTIC_ENV_VARS)
44
+
45
+
46
+ _agentic = is_agentic()
47
+
48
+ # Global console instance — plain when agentic, rich when interactive
49
+ console = Console(no_color=_agentic, highlight=not _agentic)
50
+ stderr_console = Console(stderr=True, no_color=_agentic, highlight=not _agentic)
24
51
 
25
52
  # Global output configuration
26
- _output_config = {
53
+ _output_config: dict[str, Any] = {
27
54
  "format": "default",
28
55
  "quiet": False,
29
56
  "verbose": False,
30
- "color": True,
57
+ "color": not _agentic,
58
+ "agentic": _agentic,
31
59
  }
32
60
 
33
61
 
@@ -36,11 +64,14 @@ def setup_output(
36
64
  ) -> None:
37
65
  """Setup global output configuration.
38
66
 
39
- Args:
40
- format_type: Output format (json, yaml, table, csv)
41
- quiet: Suppress non-essential output
42
- verbose: Enable verbose output
67
+ When running in an agentic/non-TTY context and no explicit format is
68
+ requested (format_type == "default"), auto-switch to JSON for
69
+ machine-friendly output.
43
70
  """
71
+ # Auto-default to JSON in agentic mode when user didn't pick a format
72
+ if format_type == "default" and _output_config["agentic"]:
73
+ format_type = "json"
74
+
44
75
  _output_config.update({"format": format_type, "quiet": quiet, "verbose": verbose})
45
76
 
46
77
  # Update console settings
@@ -273,42 +304,38 @@ def print_output(data: Any, format_type: str | None = None) -> None:
273
304
 
274
305
 
275
306
  def print_success(message: str) -> None:
276
- """Print success message.
277
-
278
- Args:
279
- message: Success message
280
- """
307
+ """Print success message."""
281
308
  if not _output_config["quiet"]:
282
- console.print(f"[green]✓[/green] {message}")
309
+ if _output_config["agentic"]:
310
+ stderr_console.print(f"OK: {message}")
311
+ else:
312
+ console.print(f"[green]✓[/green] {message}")
283
313
 
284
314
 
285
315
  def print_error(message: str) -> None:
286
- """Print error message.
287
-
288
- Args:
289
- message: Error message
290
- """
291
- stderr_console.print(f"[red]✗[/red] {message}")
316
+ """Print error message."""
317
+ if _output_config["agentic"]:
318
+ stderr_console.print(f"ERROR: {message}")
319
+ else:
320
+ stderr_console.print(f"[red]✗[/red] {message}")
292
321
 
293
322
 
294
323
  def print_warning(message: str) -> None:
295
- """Print warning message.
296
-
297
- Args:
298
- message: Warning message
299
- """
324
+ """Print warning message."""
300
325
  if not _output_config["quiet"]:
301
- console.print(f"[yellow]⚠[/yellow] {message}")
326
+ if _output_config["agentic"]:
327
+ stderr_console.print(f"WARN: {message}")
328
+ else:
329
+ console.print(f"[yellow]⚠[/yellow] {message}")
302
330
 
303
331
 
304
332
  def print_info(message: str) -> None:
305
- """Print info message.
306
-
307
- Args:
308
- message: Info message
309
- """
333
+ """Print info message."""
310
334
  if not _output_config["quiet"]:
311
- console.print(f"[blue]ℹ[/blue] {message}")
335
+ if _output_config["agentic"]:
336
+ stderr_console.print(f"INFO: {message}")
337
+ else:
338
+ console.print(f"[blue]ℹ[/blue] {message}")
312
339
 
313
340
 
314
341
  def print_debug(message: str) -> None:
@@ -43,6 +43,9 @@ class CommandMetadata:
43
43
 
44
44
  _SKILLS_DIR = Path.home() / ".deepctl" / "skills"
45
45
  _STATE_FILE = _SKILLS_DIR / "skills.json"
46
+ _REPO_CACHE_DIR = _SKILLS_DIR / "repo_cache"
47
+ _SKILLS_REPO = "deepgram/skills"
48
+ _SKILLS_BRANCH = "main"
46
49
 
47
50
 
48
51
  def get_skills_state() -> dict[str, Any]:
@@ -60,6 +63,56 @@ def save_skills_state(state: dict[str, Any]) -> None:
60
63
  _STATE_FILE.write_text(json.dumps(state, indent=2))
61
64
 
62
65
 
66
+ def fetch_repo_skills(force: bool = False) -> dict[str, str]:
67
+ """Download skill markdown files from the deepgram/skills GitHub repo.
68
+
69
+ Returns a mapping of skill name -> markdown content.
70
+ Caches locally to avoid repeated network requests.
71
+ """
72
+ import urllib.request
73
+
74
+ cache_marker = _REPO_CACHE_DIR / ".fetched"
75
+ if not force and cache_marker.exists():
76
+ # Return cached content
77
+ return _read_cached_skills()
78
+
79
+ base = f"https://raw.githubusercontent.com/{_SKILLS_REPO}/{_SKILLS_BRANCH}"
80
+ skill_names = ["api", "docs", "starters", "mcp"]
81
+ skills: dict[str, str] = {}
82
+
83
+ _REPO_CACHE_DIR.mkdir(parents=True, exist_ok=True)
84
+
85
+ for name in skill_names:
86
+ url = f"{base}/skills/{name}/SKILL.md"
87
+ try:
88
+ with urllib.request.urlopen(url, timeout=10) as resp:
89
+ content = resp.read().decode("utf-8")
90
+ skills[name] = content
91
+ ( # Cache to disk
92
+ _REPO_CACHE_DIR / f"{name}.md"
93
+ ).write_text(content)
94
+ except Exception:
95
+ # Use cached version if available
96
+ cached = _REPO_CACHE_DIR / f"{name}.md"
97
+ if cached.exists():
98
+ skills[name] = cached.read_text()
99
+
100
+ if skills:
101
+ cache_marker.write_text("1")
102
+
103
+ return skills
104
+
105
+
106
+ def _read_cached_skills() -> dict[str, str]:
107
+ """Read previously cached repo skills."""
108
+ skills: dict[str, str] = {}
109
+ if not _REPO_CACHE_DIR.exists():
110
+ return skills
111
+ for md_file in _REPO_CACHE_DIR.glob("*.md"):
112
+ skills[md_file.stem] = md_file.read_text()
113
+ return skills
114
+
115
+
63
116
  def _commands_hash(commands: list[CommandMetadata]) -> str:
64
117
  """Compute a deterministic hash of the command set."""
65
118
  blob = json.dumps(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepctl-core
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: Core components for deepctl
5
5
  Author-email: Deepgram <devrel@deepgram.com>
6
6
  Maintainer-email: Deepgram <devrel@deepgram.com>
@@ -1,305 +0,0 @@
1
- """Deepgram SDK wrapper for deepctl with authentication integration."""
2
-
3
- from pathlib import Path
4
- from typing import Any, cast
5
-
6
- from deepgram import DeepgramClient as DGClient
7
- from deepgram import DeepgramClientEnvironment
8
- from deepgram.core.api_error import ApiError
9
- from rich.console import Console
10
-
11
- from .auth import AuthManager
12
- from .config import Config
13
-
14
- console = Console()
15
-
16
-
17
- class DeepgramClient:
18
- """Wrapper around Deepgram SDK with authentication integration."""
19
-
20
- def __init__(self, config: Config, auth_manager: AuthManager):
21
- """Initialize Deepgram client.
22
-
23
- Args:
24
- config: Configuration manager
25
- auth_manager: Authentication manager
26
- """
27
- self.config = config
28
- self.auth_manager = auth_manager
29
- self._client: DGClient | None = None
30
- self._project_id: str | None = None
31
-
32
- @property
33
- def client(self) -> DGClient:
34
- """Get authenticated Deepgram client."""
35
- if self._client is None:
36
- self._client = self._create_client()
37
- return self._client
38
-
39
- def _create_client(self) -> DGClient:
40
- """Create authenticated Deepgram client."""
41
- # Ensure user is authenticated and credentials are valid
42
- self.auth_manager.guard()
43
-
44
- # Get API key and project ID
45
- api_key = self.auth_manager.get_api_key()
46
- project_id = self.auth_manager.get_project_id()
47
-
48
- if not api_key:
49
- raise ApiError(body="No API key available")
50
-
51
- # Create client with configuration
52
- current_profile = self.config.get_profile()
53
-
54
- try:
55
- kwargs: dict[str, Any] = {"api_key": api_key}
56
-
57
- # Use a custom environment when a non-default base URL is
58
- # configured.
59
- if (
60
- current_profile.base_url
61
- and current_profile.base_url != "https://api.deepgram.com"
62
- ):
63
- kwargs["environment"] = DeepgramClientEnvironment(
64
- base=current_profile.base_url,
65
- production=current_profile.base_url,
66
- agent=current_profile.base_url,
67
- )
68
-
69
- client = DGClient(**kwargs)
70
-
71
- # Store project ID for later use
72
- self._project_id = project_id
73
-
74
- return client
75
-
76
- except Exception as e:
77
- console.print(f"[red]Error creating Deepgram client:[/red] {e}")
78
- raise ApiError(body=f"Failed to create client: {e}")
79
-
80
- def transcribe_file(
81
- self,
82
- file_path: str | Path,
83
- options: dict[str, Any] | None = None,
84
- ) -> dict[str, Any]:
85
- """Transcribe an audio file.
86
-
87
- Args:
88
- file_path: Path to audio file
89
- options: Transcription options
90
-
91
- Returns:
92
- Transcription results
93
- """
94
- file_path = Path(file_path)
95
-
96
- if not file_path.exists():
97
- raise FileNotFoundError(f"Audio file not found: {file_path}")
98
-
99
- # Default options
100
- default_options: dict[str, Any] = {
101
- "model": "nova-3",
102
- "smart_format": True,
103
- "language": "en-US",
104
- }
105
-
106
- if options:
107
- default_options.update(options)
108
-
109
- try:
110
- with open(file_path, "rb") as audio_file:
111
- audio_data = audio_file.read()
112
-
113
- response = self.client.listen.v1.media.transcribe_file(
114
- request=audio_data, **default_options
115
- )
116
-
117
- return cast("dict[str, Any]", response.model_dump())
118
-
119
- except Exception as e:
120
- console.print(f"[red]Error transcribing file:[/red] {e}")
121
- raise ApiError(body=f"Transcription failed: {e}")
122
-
123
- def transcribe_url(
124
- self, url: str, options: dict[str, Any] | None = None
125
- ) -> dict[str, Any]:
126
- """Transcribe audio from URL.
127
-
128
- Args:
129
- url: URL to audio file
130
- options: Transcription options
131
-
132
- Returns:
133
- Transcription results
134
- """
135
- # Default options
136
- default_options: dict[str, Any] = {
137
- "model": "nova-3",
138
- "smart_format": True,
139
- "language": "en-US",
140
- }
141
-
142
- if options:
143
- default_options.update(options)
144
-
145
- try:
146
- response = self.client.listen.v1.media.transcribe_url(
147
- url=url, **default_options
148
- )
149
-
150
- return cast("dict[str, Any]", response.model_dump())
151
-
152
- except Exception as e:
153
- console.print(f"[red]Error transcribing URL:[/red] {e}")
154
- raise ApiError(body=f"Transcription failed: {e}")
155
-
156
- def get_projects(self) -> dict[str, Any]:
157
- """Get user's projects.
158
-
159
- Returns:
160
- Projects data
161
- """
162
- try:
163
- response = self.client.manage.v("1").get_projects()
164
- return cast("dict[str, Any]", response.model_dump())
165
-
166
- except Exception as e:
167
- console.print(f"[red]Error getting projects:[/red] {e}")
168
- raise ApiError(body=f"Failed to get projects: {e}")
169
-
170
- def get_project(self, project_id: str | None = None) -> dict[str, Any]:
171
- """Get specific project.
172
-
173
- Args:
174
- project_id: Project ID (uses configured project if not
175
- provided)
176
-
177
- Returns:
178
- Project data
179
- """
180
- if not project_id:
181
- # Ensure client is initialized which sets _project_id
182
- _ = self.client
183
- project_id = self._project_id or self.auth_manager.get_project_id()
184
-
185
- if not project_id:
186
- raise ApiError(body="No project ID available")
187
-
188
- try:
189
- response = self.client.manage.v("1").get_project(project_id)
190
- return cast("dict[str, Any]", response.model_dump())
191
-
192
- except Exception as e:
193
- console.print(f"[red]Error getting project:[/red] {e}")
194
- raise ApiError(body=f"Failed to get project: {e}")
195
-
196
- def create_project(self, name: str, company: str | None = None) -> dict[str, Any]:
197
- """Create a new project.
198
-
199
- Args:
200
- name: Project name
201
- company: Company name
202
-
203
- Returns:
204
- Created project data
205
- """
206
- try:
207
- project_data = {"name": name}
208
- if company:
209
- project_data["company"] = company
210
-
211
- response = self.client.manage.v("1").create_project(project_data)
212
- return cast("dict[str, Any]", response.model_dump())
213
-
214
- except Exception as e:
215
- console.print(f"[red]Error creating project:[/red] {e}")
216
- raise ApiError(body=f"Failed to create project: {e}")
217
-
218
- def get_usage(
219
- self,
220
- project_id: str | None = None,
221
- start_date: str | None = None,
222
- end_date: str | None = None,
223
- ) -> dict[str, Any]:
224
- """Get usage statistics.
225
-
226
- Args:
227
- project_id: Project ID (uses configured project if not
228
- provided)
229
- start_date: Start date (ISO format)
230
- end_date: End date (ISO format)
231
-
232
- Returns:
233
- Usage data
234
- """
235
- if not project_id:
236
- _ = self.client
237
- project_id = self._project_id or self.auth_manager.get_project_id()
238
-
239
- if not project_id:
240
- raise ApiError(body="No project ID available")
241
-
242
- try:
243
- params: dict[str, str] = {}
244
- if start_date:
245
- params["start"] = start_date
246
- if end_date:
247
- params["end"] = end_date
248
-
249
- response = self.client.manage.v("1").get_usage_summary(project_id, params)
250
- result = cast("dict[str, Any]", response.model_dump())
251
- result["project_id"] = project_id
252
- return result
253
-
254
- except Exception as e:
255
- console.print(f"[red]Error getting usage:[/red] {e}")
256
- raise ApiError(body=f"Failed to get usage: {e}")
257
-
258
- def get_models(self, project_id: str | None = None) -> dict[str, Any]:
259
- """Get available models.
260
-
261
- Args:
262
- project_id: Project ID (uses configured project if not
263
- provided)
264
-
265
- Returns:
266
- Models data
267
- """
268
- if not project_id:
269
- _ = self.client
270
- project_id = self._project_id or self.auth_manager.get_project_id()
271
-
272
- try:
273
- response = self.client.manage.v("1").get_project(project_id)
274
- return cast("dict[str, Any]", response.model_dump())
275
-
276
- except Exception as e:
277
- console.print(f"[red]Error getting models:[/red] {e}")
278
- raise ApiError(body=f"Failed to get models: {e}")
279
-
280
- def validate_api_key(self, api_key: str | None = None) -> bool:
281
- """Validate API key by making a simple API call.
282
-
283
- Args:
284
- api_key: API key to validate (uses configured key if not
285
- provided)
286
-
287
- Returns:
288
- True if valid, False otherwise
289
- """
290
- project_id = self.auth_manager.get_project_id()
291
- success, _, _ = self.auth_manager.verify_credentials(
292
- api_key=api_key, project_id=project_id
293
- )
294
- return success
295
-
296
- def test_connection(self) -> bool:
297
- """Test connection to Deepgram API.
298
-
299
- Returns:
300
- True if connection successful, False otherwise
301
- """
302
- success, message, _ = self.auth_manager.verify_credentials()
303
- if not success:
304
- console.print(f"[red]Connection test failed:[/red] {message}")
305
- return success
File without changes
File without changes