Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a00435c
test(telemetry): keep the test suite from sending reports
dg-coreylweathers Nov 1, 2026
18e2914
fix(cli): stop splitting hyphenated file names like debug-call.wav in…
dg-coreylweathers Nov 1, 2026
79be3b7
fix(telemetry): stop sending error reports
dg-coreylweathers Nov 1, 2026
f42280f
fix(cli): stop printing local variables in crash tracebacks
dg-coreylweathers Nov 1, 2026
bf9f329
fix(config): read and write the config file safely
dg-coreylweathers Nov 1, 2026
ccf92e2
fix(config): limit what a project deepgram.yaml or a non-default --co…
dg-coreylweathers Nov 1, 2026
7f815c1
test: run the suite as a person even when an AI agent launches pytest
dg-coreylweathers Nov 5, 2026
fccee0a
fix(auth): send keys and data only to the host you chose
dg-coreylweathers Nov 1, 2026
dd16a1e
test(config): run the config trust-root tests on macOS
dg-coreylweathers Nov 5, 2026
da53770
fix(ffprobe): never run an ffprobe found in the current directory or …
dg-coreylweathers Nov 1, 2026
500b446
fix(cli): keep ffprobe path and plugin changes out of AI agent sessions
dg-coreylweathers Nov 1, 2026
a32e5d1
fix(update): upgrade the deepgram Homebrew formula, not deepctl
dg-coreylweathers Nov 1, 2026
4c8ff33
fix(plugin): keep plugins from replacing built-in commands
dg-coreylweathers Nov 1, 2026
99216e9
fix(debug): accept probe connections only from local pages
dg-coreylweathers Nov 1, 2026
57ffd3a
fix(audio): stop reading a file name that starts with - as ffprobe op…
dg-coreylweathers Nov 1, 2026
ce4c96d
fix(auth): honor DEEPGRAM_CLI_INSECURE only for 1/true and localhost
dg-coreylweathers Nov 1, 2026
de3782e
fix(mcp): remove the SSE transport; dg mcp runs over stdio only
dg-coreylweathers Nov 1, 2026
5e0632e
fix(init): stop reading a template URL that starts with - as a git op…
dg-coreylweathers Nov 1, 2026
1fe1f03
fix(init): write .env owner-only and add it to .gitignore
dg-coreylweathers Nov 1, 2026
82d0808
fix(install): verify a pinned uv installer before running it
dg-coreylweathers Nov 1, 2026
c8ccf17
fix(plugin): make the plugin folders private
dg-coreylweathers Nov 1, 2026
a9fd048
fix(mcp): serve only Deepgram docs search, and connect only when a to…
dg-coreylweathers Nov 1, 2026
f4a6c95
docs(web): show a plugin Deepgram publishes in the plugin example
dg-coreylweathers Nov 6, 2026
c1b51be
docs: give the real order of settings and of the API key
dg-coreylweathers Nov 5, 2026
2a519bf
docs: tell plugin authors not to name an entry point like a built-in …
dg-coreylweathers Nov 5, 2026
7fb2c29
test(telemetry): run the opt-out tests on Windows without the real home
dg-coreylweathers Nov 7, 2026
98eacc9
test(plugin): isolate the trust root and plugin venv tests on Windows
dg-coreylweathers Nov 7, 2026
f969413
test(config): make the config and base URL tests pass on Windows
dg-coreylweathers Nov 7, 2026
af49535
test(ffprobe): make the ffprobe and file info tests pass on Windows
dg-coreylweathers Nov 7, 2026
cf3b165
test(config): build the deeply nested config bombs at test time
dg-coreylweathers Nov 7, 2026
c92cd19
fix(cli): escape characters the output encoding can't write
dg-coreylweathers Nov 7, 2026
55d3e98
fix(config): report a config file that can't be looked up instead of …
dg-coreylweathers Nov 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# output.py is CRLF on main and ruff format keeps it that way; let git diff --check accept it.
# output.py and plugin_manager.py are CRLF on main and ruff format keeps it that way; let git diff --check accept it.
packages/deepctl-core/src/deepctl_core/output.py whitespace=cr-at-eol
packages/deepctl-core/src/deepctl_core/plugin_manager.py whitespace=cr-at-eol
packages/deepctl-core/tests/unit/fixtures/legacy_v03/** -text
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ Register in your plugin's `pyproject.toml`:
mycommand = "my_plugin.command:MyCommand"
```

Don't reuse a built-in command name. dg ignores a plugin command named like a
built-in (for example `login`, or `audio` under `debug`) and prints a warning.
Run `dg --help` for the current names, and give your command a name specific
to your plugin so a future built-in doesn't take it.

See [`packages/deepctl-plugin-example`](packages/deepctl-plugin-example) for a complete example.

## Reporting Issues
Expand Down
96 changes: 55 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ dg requests --status failed --endpoint listen # Debug failed requests
dg api /v1/projects --jq '.projects[0].name'

# MCP server for AI editors (Claude Code, Cursor, etc.)
dg mcp --transport sse --port 8000
dg mcp
```

## Features
Expand Down Expand Up @@ -268,11 +268,11 @@ dg debug probe --port 3100 # Live stream audio analysis

### MCP Server

Connect Deepgram tools to AI coding assistants (Claude Code, Cursor, etc.).
Give AI coding assistants (Claude Code, Cursor, etc.) Deepgram documentation
search over stdio.

```bash
dg mcp # Start MCP server (stdio)
dg mcp --transport sse --port 8000 # SSE transport
```

Add to your editor's MCP config:
Expand Down Expand Up @@ -423,57 +423,71 @@ dg plugin remove <package> # Remove

Create your own — see the [plugin example](packages/deepctl-plugin-example).

## Configuration

**Priority:** CLI flags > environment variables > profile config > project config
A plugin can't replace a built-in command such as `login` or `debug audio`.
dg runs the built-in and prints a warning that names the plugin's package.
A plugin whose entry point is named like a built-in command is skipped before
it's imported, so none of its code runs; give the entry point a different name.
To use a plugin's version anyway, list the command in the user config file
at its standard location (`~/.config/deepctl/config.yaml` on Linux, or
`$XDG_CONFIG_HOME/deepctl/config.yaml` when `XDG_CONFIG_HOME` is set;
`~/Library/Application Support/deepctl/config.yaml` on macOS;
`%LOCALAPPDATA%\deepgram\deepctl\config.yaml` on Windows):

```bash
dg login # Interactive or --api-key
dg login --profile staging --api-key SK # Named profiles
dg profiles --list # List profiles
dg profiles --switch staging # Switch profile
```yaml
plugins:
allow_override:
- login
- debug audio
```

For structured output, use `--output json|yaml|table|csv` after leaf commands
that do not define their own output option, or before any command as a global
flag: `dg --output json <command>`. `dg speak --output FILE` writes audio to
`FILE`, so use `dg --output json speak ...` for Speak's structured output.

## Telemetry
dg reads this setting only from that file, never from `deepgram.yaml` or a
`--config` file. Only allow a plugin you trust: it receives everything you
pass to that command, including API keys.

The CLI phones home anonymous error reports to help us catch crashes and regressions before users have to file an issue. It's **on by default** and easy to turn off.

**What's collected:** Python exceptions, stack traces, the CLI version, and the Python runtime. Request bodies, headers, cookies, API keys, email addresses, IP addresses, and usernames are scrubbed before send. No performance traces, no profiling, no replays — errors only.
## Configuration

**Where it goes:** the `dx-cli` Sentry project owned by the Deepgram DX team.
**Priority:** CLI flags > environment variables > project `deepgram.yaml` > user
config. A project `deepgram.yaml` is read after the user config, but it can set
only `output.*` settings and a regional Deepgram `base_url` (see below).

### Opt out
The API key is the exception: dg uses `--api-key`, then the key `dg login` saved
in your system keyring, then `DEEPGRAM_API_KEY`, then an `api_key` in the config
file (where `dg login` saves it when no keyring is available).

Persistent (recommended) — add this to your `config.yaml`
(`~/.config/deepctl/config.yaml` on Linux,
`~/Library/Application Support/deepctl/config.yaml` on macOS,
`%LOCALAPPDATA%\deepgram\deepctl\config.yaml` on Windows):
dg sends your API key only to Deepgram API hosts, to hosts you list in
`api.allowed_hosts` in your own `config.yaml`, or to `DEEPGRAM_BASE_URL` (not in
an AI agent session). Anything else is refused before a request is made. Hosts
must use https: plain http works only for localhost, 127.0.0.0/8 and [::1]. For
a proxy or self-hosted deployment, add its host (and port, to approve only that
port):

```yaml
telemetry:
enabled: false
api:
allowed_hosts:
- my-proxy.example.com
- localhost:8080
```

One-shot (CI, scripts, single command):
A project's `deepgram.yaml` can't set credentials, a project ID, a profile or a
non-Deepgram `base_url`, and can't approve hosts.

```bash
DEEPCTL_TELEMETRY_DISABLED=1 dg listen recording.wav
dg login # Interactive or --api-key
dg login --profile staging --api-key SK # Named profiles
dg profiles --list # List profiles
dg profiles --switch staging # Switch profile
```

### Override the destination
For structured output, use `--output json|yaml|table|csv` after leaf commands
that do not define their own output option, or before any command as a global
flag: `dg --output json <command>`. `dg speak --output FILE` writes audio to
`FILE`, so use `dg --output json speak ...` for Speak's structured output.

For forks or self-hosted Sentry, point telemetry at your own DSN:
## Telemetry

```bash
export DEEPCTL_TELEMETRY_DSN='https://<key>@<your-sentry>/<project>'
```
This version of dg sends no telemetry: no error reports, usage data or other telemetry, whether a command succeeds or fails. dg still contacts the Deepgram API for the commands you run, and pypi.org to check for updates.

`DEEPCTL_TELEMETRY_DISABLED` always wins. The default DSN is baked into the package and only used when the override is unset.
The `DEEPCTL_TELEMETRY_DISABLED` environment variable and the `telemetry.enabled` setting in `config.yaml` are still accepted, and the new `--no-telemetry` flag is accepted too, but none of them has an effect in this version. `DEEPCTL_TELEMETRY_DSN` is now ignored unless it points at the local machine, which is for tests. The last line of `dg --help` says that dg sends no telemetry.

## Development

Expand Down Expand Up @@ -505,7 +519,7 @@ cli/
│ ├── deepctl-cmd-keys/ # API keys management command for deepctl
│ ├── deepctl-cmd-listen/ # Listen (live speech-to-text) command for deepctl
│ ├── deepctl-cmd-login/ # Login command for deepctl
│ ├── deepctl-cmd-mcp/ # MCP proxy command for deepctl — connects to Deepgram's developer API
│ ├── deepctl-cmd-mcp/ # MCP server for deepctl: Deepgram documentation search over stdio
│ ├── deepctl-cmd-members/ # Members management command for deepctl
│ ├── deepctl-cmd-models/ # Models command for deepctl
│ ├── deepctl-cmd-plugin/ # Plugin management command for deepctl
Expand All @@ -520,7 +534,7 @@ cli/
│ ├── deepctl-core/ # Core components for deepctl
│ ├── deepctl-plugin-example/ # Example plugin for deepctl
│ ├── deepctl-shared-utils/ # Shared utilities for deepctl
│ └── deepctl-telemetry/ # Opt-out phone-home telemetry for deepctl
│ └── deepctl-telemetry/ # Error reporting for deepctl; this version sends no telemetry
├── tests/ # Integration tests
└── Makefile # Development tasks
```
Expand All @@ -546,7 +560,7 @@ cli/
| `deepctl listen` | Listen (live speech-to-text) command for deepctl |
| `deepctl login` | Login command for deepctl |
| `deepctl logout` | Login command for deepctl |
| `deepctl mcp` | MCP proxy command for deepctl — connects to Deepgram's developer API |
| `deepctl mcp` | MCP server for deepctl: Deepgram documentation search over stdio |
| `deepctl members` | Members management command for deepctl |
| `deepctl models` | Models command for deepctl |
| `deepctl plugin` | Plugin management command for deepctl |
Expand Down Expand Up @@ -581,7 +595,7 @@ cli/
| [`deepctl-cmd-keys`](packages/deepctl-cmd-keys) | API keys management command for deepctl |
| [`deepctl-cmd-listen`](packages/deepctl-cmd-listen) | Listen (live speech-to-text) command for deepctl |
| [`deepctl-cmd-login`](packages/deepctl-cmd-login) | Login command for deepctl |
| [`deepctl-cmd-mcp`](packages/deepctl-cmd-mcp) | MCP proxy command for deepctl — connects to Deepgram's developer API |
| [`deepctl-cmd-mcp`](packages/deepctl-cmd-mcp) | MCP server for deepctl: Deepgram documentation search over stdio |
| [`deepctl-cmd-members`](packages/deepctl-cmd-members) | Members management command for deepctl |
| [`deepctl-cmd-models`](packages/deepctl-cmd-models) | Models command for deepctl |
| [`deepctl-cmd-plugin`](packages/deepctl-cmd-plugin) | Plugin management command for deepctl |
Expand All @@ -596,7 +610,7 @@ cli/
| [`deepctl-core`](packages/deepctl-core) | Core components for deepctl |
| [`deepctl-plugin-example`](packages/deepctl-plugin-example) | Example plugin for deepctl |
| [`deepctl-shared-utils`](packages/deepctl-shared-utils) | Shared utilities for deepctl |
| [`deepctl-telemetry`](packages/deepctl-telemetry) | Opt-out phone-home telemetry for deepctl |
| [`deepctl-telemetry`](packages/deepctl-telemetry) | Error reporting for deepctl; this version sends no telemetry |
<!-- END:packages -->

## Release
Expand Down
48 changes: 48 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,64 @@
Ensures tests run in a clean environment by stripping DEEPGRAM_*
environment variables before each test. Tests that need specific
env vars should use @patch.dict("os.environ", {"DEEPGRAM_API_KEY": "..."}).

Telemetry is disabled for every test so a test that drives `main()` never
initializes the real telemetry client. Telemetry's own tests opt back in.
"""

import os

import pytest


@pytest.fixture(autouse=True)
def _reset_shown_hints():
"""A hint prints once per run (process); each test is its own run."""
from deepctl_core import output

output._hints_shown.clear()
yield
output._hints_shown.clear()


@pytest.fixture(autouse=True)
def _reset_config_parse_cache():
"""Config files are parsed once per process; each test is its own run."""
from deepctl_core import config

config._parse_cache.clear()
yield
config._parse_cache.clear()


@pytest.fixture(autouse=True)
def _clean_ai_agent_env(monkeypatch):
"""Run as a person would, even when an AI coding agent launched pytest.

Agent markers (AI_AGENT_ENV_MARKERS, Aider's OR_*) switch dg into its
agent trust mode. Tests that cover that mode set them, or patch
is_ai_agent, themselves.
"""
from deepctl_core.output import AI_AGENT_ENV_MARKERS

for key in (*AI_AGENT_ENV_MARKERS, "OR_APP_NAME", "OR_SITE_URL"):
monkeypatch.delenv(key, raising=False)


@pytest.fixture(autouse=True)
def _clean_deepgram_env(monkeypatch):
"""Remove all DEEPGRAM_* environment variables for test isolation."""
for key in list(os.environ):
if key.startswith("DEEPGRAM_"):
monkeypatch.delenv(key, raising=False)


@pytest.fixture(autouse=True)
def _disable_telemetry(monkeypatch):
"""Keep tests that call `main()` from initializing real telemetry.

The destination also points at a closed loopback port, so a test that
re-enables telemetry can never reach a real endpoint.
"""
monkeypatch.setenv("DEEPCTL_TELEMETRY_DISABLED", "1")
monkeypatch.setenv("DEEPCTL_TELEMETRY_DSN", "http://k@127.0.0.1:9/1")
Loading
Loading