deepctl-core 0.2.14__tar.gz → 0.2.17__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 (22) hide show
  1. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/PKG-INFO +3 -3
  2. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/README.md +1 -1
  3. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/pyproject.toml +2 -2
  4. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core/__init__.py +5 -0
  5. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core/base_command.py +53 -5
  6. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core/client.py +9 -0
  7. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core/output.py +40 -0
  8. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core/plugin_manager.py +78 -3
  9. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core/skill_generator.py +11 -2
  10. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core.egg-info/PKG-INFO +3 -3
  11. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core.egg-info/requires.txt +1 -1
  12. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/setup.cfg +0 -0
  13. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core/auth.py +0 -0
  14. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core/base_group_command.py +0 -0
  15. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core/config.py +0 -0
  16. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core/models.py +0 -0
  17. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core/plugin_env.py +0 -0
  18. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core/py.typed +0 -0
  19. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core/timing.py +0 -0
  20. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core.egg-info/SOURCES.txt +0 -0
  21. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/src/deepctl_core.egg-info/dependency_links.txt +0 -0
  22. {deepctl_core-0.2.14 → deepctl_core-0.2.17}/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.14
3
+ Version: 0.2.17
4
4
  Summary: Core components for deepctl
5
5
  Author-email: Deepgram <devrel@deepgram.com>
6
6
  Maintainer-email: Deepgram <devrel@deepgram.com>
@@ -16,7 +16,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
16
  Requires-Python: >=3.10
17
17
  Description-Content-Type: text/markdown
18
18
  Requires-Dist: click>=8.0.0
19
- Requires-Dist: deepgram-sdk>=7.5.0
19
+ Requires-Dist: deepgram-sdk<8,>=7.7.0
20
20
  Requires-Dist: pydantic>=2.0.0
21
21
  Requires-Dist: rich>=13.0.0
22
22
  Requires-Dist: httpx>=0.24.0
@@ -63,7 +63,7 @@ pipx run deepctl --help
63
63
  ## Dependencies
64
64
 
65
65
  - `click>=8.0.0`
66
- - `deepgram-sdk>=6.0.0rc2`
66
+ - `deepgram-sdk>=7.7.0,<8`
67
67
  - `pydantic>=2.0.0`
68
68
  - `rich>=13.0.0`
69
69
  - `httpx>=0.24.0`
@@ -30,7 +30,7 @@ pipx run deepctl --help
30
30
  ## Dependencies
31
31
 
32
32
  - `click>=8.0.0`
33
- - `deepgram-sdk>=6.0.0rc2`
33
+ - `deepgram-sdk>=7.7.0,<8`
34
34
  - `pydantic>=2.0.0`
35
35
  - `rich>=13.0.0`
36
36
  - `httpx>=0.24.0`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "deepctl-core"
7
- version = "0.2.14" # x-release-please-version
7
+ version = "0.2.17" # x-release-please-version
8
8
  description = "Core components for deepctl"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -23,7 +23,7 @@ keywords = ["deepgram", "core", "auth", "config", "client"]
23
23
  requires-python = ">=3.10"
24
24
  dependencies = [
25
25
  "click>=8.0.0",
26
- "deepgram-sdk>=7.5.0",
26
+ "deepgram-sdk>=7.7.0,<8",
27
27
  "pydantic>=2.0.0",
28
28
  "rich>=13.0.0",
29
29
  "httpx>=0.24.0",
@@ -16,6 +16,7 @@ from .output import (
16
16
  OutputFormatter,
17
17
  get_console,
18
18
  get_output_format,
19
+ get_status_console,
19
20
  is_agentic,
20
21
  print_error,
21
22
  print_info,
@@ -23,6 +24,7 @@ from .output import (
23
24
  print_success,
24
25
  print_warning,
25
26
  setup_output,
27
+ update_output,
26
28
  )
27
29
  from .plugin_manager import PluginManager
28
30
  from .timing import (
@@ -50,6 +52,8 @@ __all__ = [
50
52
  "TimingContext",
51
53
  "enable_timing",
52
54
  "get_console",
55
+ "get_output_format",
56
+ "get_status_console",
53
57
  "get_timing_summary",
54
58
  "is_agentic",
55
59
  "is_timing_enabled",
@@ -60,4 +64,5 @@ __all__ = [
60
64
  "print_timing_summary",
61
65
  "print_warning",
62
66
  "setup_output",
67
+ "update_output",
63
68
  ]
@@ -29,6 +29,19 @@ class BaseCommand(ABC):
29
29
  requires_project: bool = False
30
30
  ci_friendly: bool = True
31
31
 
32
+ # Result status -> process exit code, per the contract published in
33
+ # llms-full.txt: 0 = success, 1 = error, 2 = user interrupt. A command that
34
+ # reports failure must not exit 0 -- scripts and CI branch on the exit code,
35
+ # not on the "status" field inside the payload. "cancelled" is 2 rather than
36
+ # the shell's 130 to match that contract and main.py's own KeyboardInterrupt
37
+ # handler; `dg mcp` in particular returns cancelled straight out of its
38
+ # KeyboardInterrupt path. Any status not listed here exits 0
39
+ # (success, succeeded, info, dry_run, warning).
40
+ EXIT_CODES: ClassVar[dict[str, int]] = {
41
+ "error": 1,
42
+ "cancelled": 2,
43
+ }
44
+
32
45
  # Agent-oriented metadata
33
46
  examples: ClassVar[list[str]] = []
34
47
  agent_help: str = ""
@@ -147,6 +160,23 @@ class BaseCommand(ABC):
147
160
  stderr_console.print_exception()
148
161
  raise click.ClickException(str(e))
149
162
 
163
+ # Payload is already on stdout; now make the exit code agree with it.
164
+ exit_code = self.exit_code_for(result)
165
+ if exit_code:
166
+ raise SystemExit(exit_code)
167
+
168
+ def exit_code_for(self, result: Any) -> int:
169
+ """Map a command result to a process exit code.
170
+
171
+ Args:
172
+ result: Command result (may be None)
173
+
174
+ Returns:
175
+ Exit code for this result's status; 0 when it reported no failure
176
+ """
177
+ status = str(getattr(result, "status", "") or "")
178
+ return self.EXIT_CODES.get(status, 0)
179
+
150
180
  def is_guided(self, ctx: click.Context) -> bool:
151
181
  """True only when the user invoked this command with no input at all.
152
182
 
@@ -285,9 +315,25 @@ class BaseCommand(ABC):
285
315
  elif output_format == "csv":
286
316
  self._output_csv(result)
287
317
  else:
288
- console.print(f"[red]Unknown output format:[/red] {output_format}")
318
+ # Diagnostics belong on stderr even here, or the complaint about
319
+ # the format corrupts the payload we fall back to.
320
+ stderr_console.print(
321
+ f"[red]Unknown output format:[/red] {output_format}"
322
+ )
289
323
  self._output_json(result)
290
324
 
325
+ def _write_payload(self, text: str) -> None:
326
+ """Write already-serialised machine-readable text to stdout verbatim.
327
+
328
+ Rich's default print would treat square brackets as style markup and
329
+ silently delete them, so a key comment of "[ci] runner" would lose the
330
+ "[ci]". It would also hard-wrap at the console width, injecting newlines
331
+ into the middle of a value. Both corrupt the payload, so markup,
332
+ highlighting and wrapping are all off. Routed through `console` rather
333
+ than `sys.stdout` so `--quiet` still suppresses output.
334
+ """
335
+ console.print(text, markup=False, highlight=False, soft_wrap=True)
336
+
291
337
  def _output_json(self, result: Any) -> None:
292
338
  """Output result as JSON."""
293
339
  import json
@@ -310,9 +356,11 @@ class BaseCommand(ABC):
310
356
  import yaml
311
357
 
312
358
  if isinstance(result, dict | list):
313
- console.print(yaml.dump(result, default_flow_style=False))
359
+ self._write_payload(yaml.dump(result, default_flow_style=False))
314
360
  else:
315
- console.print(yaml.dump({"result": str(result)}, default_flow_style=False))
361
+ self._write_payload(
362
+ yaml.dump({"result": str(result)}, default_flow_style=False)
363
+ )
316
364
 
317
365
  def _output_table(self, result: Any) -> None:
318
366
  """Output result as table."""
@@ -359,7 +407,7 @@ class BaseCommand(ABC):
359
407
  dict_writer = csv.DictWriter(output, fieldnames=result[0].keys())
360
408
  dict_writer.writeheader()
361
409
  dict_writer.writerows(result)
362
- console.print(output.getvalue())
410
+ self._write_payload(output.getvalue())
363
411
 
364
412
  elif isinstance(result, dict):
365
413
  # Dictionary
@@ -368,7 +416,7 @@ class BaseCommand(ABC):
368
416
  writer.writerow(["Key", "Value"])
369
417
  for key, value in result.items():
370
418
  writer.writerow([key, value])
371
- console.print(output.getvalue())
419
+ self._write_payload(output.getvalue())
372
420
 
373
421
  else:
374
422
  # Fallback to JSON
@@ -189,12 +189,17 @@ class DeepgramClient:
189
189
  model: str,
190
190
  encoding: str | None = None,
191
191
  sample_rate: float | None = None,
192
+ speed: float | None = None,
193
+ expressivity: int | None = None,
192
194
  ) -> Iterator[bytes]:
193
195
  """Stream TTS audio over the Flux v2 WebSocket (speak.v2.connect).
194
196
 
195
197
  Yields raw audio chunks as they arrive. The streaming transport emits
196
198
  raw (non-containerized) audio, so only linear16/mulaw/alaw encodings
197
199
  apply and sample_rate is sent as the string the streaming API expects.
200
+
201
+ ``speed`` (0.85-1.15) and beta ``expressivity`` (-2..2, default 0) are
202
+ Flux connect query parameters; they are only sent when set.
198
203
  """
199
204
  from deepgram.speak.v2.types.speak_v2speak import SpeakV2Speak
200
205
 
@@ -203,6 +208,10 @@ class DeepgramClient:
203
208
  connect_kwargs["encoding"] = encoding
204
209
  if sample_rate:
205
210
  connect_kwargs["sample_rate"] = str(int(sample_rate))
211
+ if speed is not None:
212
+ connect_kwargs["speed"] = speed
213
+ if expressivity is not None:
214
+ connect_kwargs["expressivity"] = expressivity
206
215
 
207
216
  try:
208
217
  with self.client.speak.v2.connect(**connect_kwargs) as conn:
@@ -103,6 +103,30 @@ def setup_output(
103
103
  console.quiet = quiet
104
104
 
105
105
 
106
+ def update_output(
107
+ format_type: str | None = None,
108
+ quiet: bool | None = None,
109
+ verbose: bool | None = None,
110
+ ) -> None:
111
+ """Update parts of the global output configuration in place.
112
+
113
+ Unlike setup_output(), only the arguments actually provided change;
114
+ every other setting keeps its current value. The per-command
115
+ pass-through options use this so that ``dg models -o json`` has the
116
+ same effect as ``dg -o json models`` without resetting the quiet or
117
+ verbose state the root group already applied.
118
+ """
119
+ if format_type is not None:
120
+ if format_type == "default" and _output_config["agentic"]:
121
+ format_type = "json"
122
+ _output_config["format"] = format_type
123
+ if quiet is not None:
124
+ _output_config["quiet"] = quiet
125
+ console.quiet = quiet
126
+ if verbose is not None:
127
+ _output_config["verbose"] = verbose
128
+
129
+
106
130
  class OutputFormatter:
107
131
  """Formatter for different output types."""
108
132
 
@@ -505,3 +529,19 @@ def get_console() -> Console:
505
529
  Console instance
506
530
  """
507
531
  return console
532
+
533
+
534
+ def get_status_console() -> Console:
535
+ """Get the console for status/progress chrome.
536
+
537
+ Always writes to stderr, so anything printed through it can never corrupt
538
+ the machine-readable payload a command writes to stdout. Commands should
539
+ use this rather than declaring their own ``Console(stderr=True)`` — a
540
+ per-command console silently misses the agentic no-color settings, and a
541
+ command that reaches for a bare ``Console()`` reintroduces the stdout
542
+ pollution this exists to prevent.
543
+
544
+ Returns:
545
+ Console instance writing to stderr
546
+ """
547
+ return stderr_console
@@ -7,12 +7,11 @@ from pathlib import Path
7
7
  from typing import Any, cast
8
8
 
9
9
  import click
10
- from rich.console import Console
11
10
 
12
11
  from .base_command import BaseCommand
13
12
  from .base_group_command import BaseGroupCommand
14
13
  from .models import ErrorResult, PluginInfo
15
- from .output import print_warning
14
+ from .output import print_warning, stderr_console
16
15
  from .plugin_env import (
17
16
  PLUGIN_VENV,
18
17
  get_plugin_state,
@@ -21,7 +20,9 @@ from .plugin_env import (
21
20
  )
22
21
  from .timing import TimingContext
23
22
 
24
- console = Console()
23
+ # Load errors are diagnostics: they go to stderr so a broken plugin can't
24
+ # corrupt `dg ... -o json` payloads on stdout.
25
+ console = stderr_console
25
26
 
26
27
 
27
28
  class PluginManager:
@@ -293,6 +294,80 @@ class PluginManager:
293
294
  help="Skip interactive prompts; use defaults for any optional features.",
294
295
  )(cmd)
295
296
 
297
+ # Accept the global output options after the subcommand as well.
298
+ cmd = self._add_global_passthrough_options(cmd)
299
+
300
+ return cmd
301
+
302
+ def _add_global_passthrough_options(self, cmd: click.Command) -> click.Command:
303
+ """Let the global output options work after the subcommand too.
304
+
305
+ The root group defines -o/--output, -q/--quiet and -v/--verbose, but
306
+ Click only parses group options that appear *before* the subcommand
307
+ name — so ``dg models -o json`` used to fail with "No such option
308
+ '-o'" while ``dg -o json models`` worked, and the error gave no hint.
309
+ These pass-through copies apply the same effect from the subcommand
310
+ position. A copy is only added when the command does not already
311
+ define the option name itself (for example, ``dg speak --output``
312
+ names an audio file and keeps its own meaning).
313
+ """
314
+ from .output import update_output
315
+
316
+ taken: set[str] = set()
317
+ for param in cmd.params:
318
+ taken.update(param.opts)
319
+ taken.update(param.secondary_opts)
320
+
321
+ def _apply_format(
322
+ _ctx: click.Context, _param: click.Parameter, value: str | None
323
+ ) -> None:
324
+ if value is not None:
325
+ update_output(format_type=value)
326
+
327
+ def _apply_quiet(
328
+ _ctx: click.Context, _param: click.Parameter, value: bool
329
+ ) -> None:
330
+ if value:
331
+ update_output(quiet=True)
332
+
333
+ def _apply_verbose(
334
+ _ctx: click.Context, _param: click.Parameter, value: bool
335
+ ) -> None:
336
+ if value:
337
+ update_output(verbose=True)
338
+
339
+ if not {"--output", "-o"} & taken:
340
+ cmd = click.option(
341
+ "--output",
342
+ "-o",
343
+ type=click.Choice(
344
+ ["json", "yaml", "table", "csv"], case_sensitive=False
345
+ ),
346
+ expose_value=False,
347
+ callback=_apply_format,
348
+ help="Output format (same as the global -o before the command).",
349
+ )(cmd)
350
+
351
+ if not {"--quiet", "-q"} & taken:
352
+ cmd = click.option(
353
+ "--quiet",
354
+ "-q",
355
+ is_flag=True,
356
+ expose_value=False,
357
+ callback=_apply_quiet,
358
+ help="Suppress non-essential output.",
359
+ )(cmd)
360
+
361
+ if not {"--verbose", "-v"} & taken:
362
+ cmd = click.option(
363
+ "--verbose",
364
+ "-v",
365
+ is_flag=True,
366
+ expose_value=False,
367
+ callback=_apply_verbose,
368
+ help="Enable verbose output.",
369
+ )(cmd)
370
+
296
371
  return cmd
297
372
 
298
373
  def _build_help_text(self, instance: Any) -> str:
@@ -417,7 +417,8 @@ def render_developer_guide(
417
417
  lines.append("## Text-to-Speech (TTS)")
418
418
  lines.append("")
419
419
  lines.append(
420
- "Generate natural-sounding speech from text using Deepgram's Aura voices."
420
+ "Generate natural-sounding speech from text using Deepgram's Aura and "
421
+ "Flux voices."
421
422
  )
422
423
  lines.append("")
423
424
  lines.append("### Models")
@@ -463,6 +464,10 @@ def render_developer_guide(
463
464
  lines.append("")
464
465
  lines.append("### Flux TTS — Speak v2, WebSocket streaming (Python)")
465
466
  lines.append("")
467
+ lines.append(
468
+ "`expressivity` is beta and defaults to `0` (nominal delivery) when omitted."
469
+ )
470
+ lines.append("")
466
471
  lines.append("```python")
467
472
  lines.append("from deepgram import DeepgramClient")
468
473
  lines.append("from deepgram.speak.v2.types.speak_v2speak import SpeakV2Speak")
@@ -470,7 +475,11 @@ def render_developer_guide(
470
475
  lines.append('client = DeepgramClient(api_key="DEEPGRAM_API_KEY")')
471
476
  lines.append("")
472
477
  lines.append("with client.speak.v2.connect(")
473
- lines.append(' model="flux-alexis-en", encoding="linear16", sample_rate="24000"')
478
+ lines.append(' model="flux-alexis-en",')
479
+ lines.append(' encoding="linear16",')
480
+ lines.append(' sample_rate="24000",')
481
+ lines.append(" speed=1.0, # 0.85–1.15 in 0.05 steps (optional)")
482
+ lines.append(" expressivity=0, # beta; -2..2, default 0 = nominal (optional)")
474
483
  lines.append(") as conn:")
475
484
  lines.append(
476
485
  ' conn.send_speak(SpeakV2Speak(type="Speak", text="Hello from Flux!"))'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepctl-core
3
- Version: 0.2.14
3
+ Version: 0.2.17
4
4
  Summary: Core components for deepctl
5
5
  Author-email: Deepgram <devrel@deepgram.com>
6
6
  Maintainer-email: Deepgram <devrel@deepgram.com>
@@ -16,7 +16,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
16
  Requires-Python: >=3.10
17
17
  Description-Content-Type: text/markdown
18
18
  Requires-Dist: click>=8.0.0
19
- Requires-Dist: deepgram-sdk>=7.5.0
19
+ Requires-Dist: deepgram-sdk<8,>=7.7.0
20
20
  Requires-Dist: pydantic>=2.0.0
21
21
  Requires-Dist: rich>=13.0.0
22
22
  Requires-Dist: httpx>=0.24.0
@@ -63,7 +63,7 @@ pipx run deepctl --help
63
63
  ## Dependencies
64
64
 
65
65
  - `click>=8.0.0`
66
- - `deepgram-sdk>=6.0.0rc2`
66
+ - `deepgram-sdk>=7.7.0,<8`
67
67
  - `pydantic>=2.0.0`
68
68
  - `rich>=13.0.0`
69
69
  - `httpx>=0.24.0`
@@ -1,5 +1,5 @@
1
1
  click>=8.0.0
2
- deepgram-sdk>=7.5.0
2
+ deepgram-sdk<8,>=7.7.0
3
3
  pydantic>=2.0.0
4
4
  rich>=13.0.0
5
5
  httpx>=0.24.0
File without changes