Skip to content

0.5.0 (5/5): dg mcp, agent mode, CLI and config fixes, and the 0.5.0 release marker - #164

Closed
dg-coreylweathers wants to merge 25 commits into
release/0.5.0-4-init-outputfrom
release/0.5.0-5-mcp-agent
Closed

dg-coreylweathers wants to merge 25 commits into
release/0.5.0-4-init-outputfrom
release/0.5.0-5-mcp-agent

Conversation

@dg-coreylweathers

Copy link
Copy Markdown
Contributor

Part 5 of 5 of the 0.5.0 release, stacked on part 4. Review in order. Merge only after the PR below it has merged, and only after 0.4.1 is released.

dg mcp shares one proxy and tool table and keeps stdout for JSON-RPC from the start. AI agents are detected from one documented table, and dg help agent-mode explains agent mode. Warnings stay off stdout, config fixes (XDG on macOS, locked config files), Flux model naming, per-OS stdin and microphone help, and coded messages for cancel and unexpected errors. Ends with review fix-ups (a plugin named like a built-in alias is refused before import, dg login --complete checks the base URL, test and README fixes) and chore: release 0.5.0, which carries Release-As: 0.5.0.

Commits

  • fcc4381 refactor(mcp): share one proxy and tool table for every dg mcp path
  • efebe2a fix(mcp): keep stdout for JSON-RPC from the start of dg mcp
  • ade3f5e docs(mcp): describe dg mcp as the documentation search it is
  • 4e39fad feat(core): detect AI agents from one documented table
  • 891f16e feat(help): explain agent mode with dg help agent-mode
  • 155263c fix(cli): keep warnings and notices off stdout
  • dfed13b style: say what each check does in config and skills comments
  • a6c2847 fix(cli): escape characters the output encoding can't write
  • 59c66e1 fix(login): show a profile's config-file key in profiles --list
  • a82791a fix(config): count ~/.config on macOS when XDG_CONFIG_HOME names it
  • 47aceb9 fix(config): wait for a locked config file once per run
  • 1f1c66d fix(listen,speak): name Flux models, not API versions
  • 366017b fix(listen): give each OS its own stdin and microphone help
  • 2f690c7 test: give timing checks room on a busy host
  • f83a8c6 fix(cli): use coded messages for cancel and unexpected errors
  • 930e345 test(debug-probe): run the ignored-SIGINT check only on Linux
  • 4bc6cc7 fix(plugin): refuse a plugin named like a built-in alias before importing it
  • 2df0098 fix(login): check the base URL before dg login --complete saves a key
  • 02ca0e2 test: merge the two fixtures that clear AI agent markers
  • 4eac835 docs: compare with 0.4.1 in comments, the release before 0.5.0
  • 8ffaa27 build: regenerate the stale package READMEs and run the generator with uv
  • 01b5615 docs(telemetry): say this version sends no telemetry
  • 7cb4b00 test(cli): patch both built-in tables in the missing-package test
  • 30b7177 test(mcp): let the docs lint pass the entry point's transport refusal
  • 28234d4 chore: release 0.5.0

Notes

  • Tests for the full 0.5.0 head (Docker, Linux): Python 3.10 5,616 passed, 91 skipped, 0 failed; Python 3.14 5,617 passed, 90 skipped, 0 failed; lint, types, lock, floors, READMEs and build pass. CI on this PR runs the full matrix.
  • After 0.4.1's release PR merges, this stack is rebased onto main and the lockstep versions are re-baselined to the versions that release set.
  • Merge with a merge commit, not a squash: release-please reads the individual commits.
  • The last commit carries Release-As: 0.5.0. Without it, release-please would propose a patch release instead of 0.5.0.

🤖 Generated with Claude Code

dg-coreylweathers and others added 25 commits November 6, 2026 20:56
The base runs dg mcp from a minimal copy of the vendored proxy in
deepctl_cmd_mcp.proxy. dg mcp's fast path and the command now share it,
and the port is finished:

- initialize and tools/list come from one fixed local instructions and
  tool table, in deepctl.mcp._fast; the proxy's own copy goes, so the
  two paths can't drift;
- the fast path's hand-off serves through the proxy (its LineWriter
  drops the reply to the replayed initialize) instead of its own
  adapter, so nothing connects until a tool call and a remote failure
  is that call's tool error;
- key lookup (key_or_error) lives in the hand-off module and serves
  both paths: no key, an unreadable config or a config key an agent
  session doesn't trust give the same text;
- the HTTP client takes the transport's no-cookies setting; it still
  follows no redirects, and a 3xx keeps core's refusal text.

The base's tests that duplicate these now live in test_handoff.py; the
rest stay in test_proxy.py, pointed at the shared table. The base's
end-to-end tests (with the --debug and same-origin redirect cases) are
kept as they are.
Every dg mcp run that serves (not --help or --agent-friendly), with or
without root options before mcp, now moves fd 1 to stderr before Click
or any command code loads. The MCP server writes to a private copy of
the original stdout, so a stray print, a library warning, a config
notice or a child process can no longer corrupt the client's JSON-RPC
stream.

main() recognises `dg [root options] mcp` without Click; a test keeps
its list of value-taking root options equal to the root group's.
The help, the READMEs, llms.txt, llms-full.txt and the landing page
promised SSE and streamable HTTP transports, "every CLI command" as
tools and direct API access, and one README put the API key in a
project .mcp.json's args. Agents follow this text literally.

Now they all say what the command does: a local stdio MCP server with
one tool, search_deepgram_knowledge_sources, that searches Deepgram's
documentation with the key from dg login or DEEPGRAM_API_KEY. The key
never goes in the MCP config, a missing key is a tool error rather
than a failed start (or, for a config file dg can't trust, an error
that asks a person to fix it), and Claude Desktop's literal env values
are called out. The --base-url and --debug help match their behaviour,
including https unless the host is loopback.

The help spec is regenerated with --write-help; the cmd-mcp README
matches generate_readmes.py output for the new description.

A test fails on MCP text that promises what dg mcp doesn't do. It
scans the READMEs, web/public, web/src and the sources (help specs
included) for:

- a transport other than stdio in any spelling: --transport sse,
  --transport=http, -t sse, "--transport", "sse" in args, an /sse URL,
  streamable-http, "stdio/SSE";
- a --stdout option;
- an API key on a command line (--api-key KEY, --api-key=KEY, a
  variable) or in an MCP config's args array, however it is wrapped.

Only the API-key checks skip `dg login --api-key ...`, which gives the
key to the command that stores it; every other check still applies on
that line. Parametrized cases cover each variant and the true text
that must pass.
deepctl_core.agents is now the only place dg decides whether an AI
agent, automation or a person is running it. Each hard signal names
the public source that documents or implements it, and the table
grows from 9 variables to 21 rows: AI_AGENT first, then AGENT (amp,
goose), GOOSE_TERMINAL, the Claude Code, Codex, Gemini, Copilot CLI,
OpenCode, Antigravity, Augment, Cursor and Aider variables, the
/opt/.devin file and a .pi/agent entry in PATH. A variable counts
when it is set and not empty; whitespace still counts, as before.

DG_AGENT=1 turns agent mode on and DG_AGENT=0 turns it off for
output, prompts, hints and login. Trust decisions keep reading the
raw markers, so DG_AGENT never loosens or tightens which config file
and key dg uses. Every new marker therefore also applies the
agent-session config and base URL rules.

NO_COLOR no longer counts toward the no-terminal score. Under
NO_COLOR, and in agent mode, the consoles write plain text with no
colour, bold or dim; otherwise they never pass no_color=False, so
FORCE_COLOR and TERM work as rich reads them. CI matches
case-insensitively, and the command-line flags count only before
"--".

output.is_agentic(), output.is_ai_agent() and AI_AGENT_ENV_MARKERS
stay importable and keep their meaning. output gains invoked_prog()
and stderr_color() for messages that name the program or colour raw
stderr lines.
"dg help agent-mode" prints, as plain text and offline:

- the detection table, each row with the sources that document it;
- which row this process matched, and why agent mode is on or off;
- how a person removes a stray marker in sh/bash/zsh, fish,
  PowerShell and cmd, and DG_AGENT=0 for the markers that can't be
  unset;
- the trust rules in agent sessions: a user config that others can
  change or another user owns is ignored, its API key unused and
  never saved to, DG_AGENT=0 doesn't change that, and the person runs
  chmod 600 on it.

"dg help" is a new command group from the root distribution, added
as manifest fragments with root entry points. The README points to
the topic instead of listing a few variables.
Every notice now goes to stderr, prints once, and names the program
the way it was invoked:

- A person at a terminal whose environment carries an AI-agent
  marker gets one INFO line saying why dg is in agent mode and
  pointing to "dg help agent-mode". It names no shell command, and
  it never prints when DG_AGENT decides the mode or without a
  terminal.
- The skills prompt, the update notice and the plugin update notice
  no longer show in agent or automation sessions, use colour only on
  a person's terminal (NO_COLOR and TERM=dumb turn it off,
  FORCE_COLOR turns it on), and name the invoked program.
- Copying a legacy config is reported through the config notice
  queue, so it follows the INFO:/WARN: prefixes and --quiet.
- dg ffprobe --path reports a bad path through the error console,
  with the ERROR: prefix in agent mode, once.
- The pending-login profile error names the invoked program.
- dg --version prints "dg, version ...", "deepgram, version ..." or
  "deepctl, version ..." as invoked (DEEPCTL_PROG_NAME first, as for
  every other message), on the fast path and the Click path alike.
  Changelog: "--version now prints the name you ran (dg, deepgram or
  deepctl)". The bug report template's example follows.

New subprocess tests pin -o json stdout to one JSON document with a
malformed or unreadable config, with the warning once on stderr.
A few comments and test docstrings in the config loading, credential
and skills code pointed at notes kept outside the repository. They now
describe the behaviour under test instead.
On Windows a redirected stdout uses the ANSI code page (cp1252, say).
The banner's block art and the check mark aren't in it, so `dg --help >
file` and the `dg ffprobe --path` success line stopped with "'charmap'
codec can't encode". A stdout or stderr that isn't UTF-8, and whose
error handler raises on such characters (strict, or surrogateescape
under a C locale), now writes them as backslash escapes. The encoding
itself is kept, so a real console keeps the one Python chose, and a
UTF-8 stream is untouched.
profiles --list and --current read each profile's key from the keyring
and used the config file's key only when the keyring raised. A backend
that answers "no entry", as Windows Credential Manager does for a key
saved only in config.yaml, showed "API Key: Not set" for a profile that
has one. The config file's key is now shown whenever the keyring has
none.
On macOS platformdirs puts the user config under $XDG_CONFIG_HOME when
it is set, but only ~/Library/Application Support could approve hosts
in an agent session. A person with XDG_CONFIG_HOME=~/.config, a common
setup, had approved hosts that never counted there.

When XDG_CONFIG_HOME is exactly ~/.config under the account's own home
(taken from the OS, not $HOME), that file is the one that counts, as on
Linux. Any other value still keeps Application Support, so a file
elsewhere never counts in an agent session.
A read waits up to 5 s for a writer's lock on the config file, then
skips the file with a warning. A command reads the file more than once,
so a writer stuck holding the lock made `dg profiles --list` wait two or
three times as long (about 16 s on macOS). Once a wait for a file has
timed out, later reads of that file in the same run try the lock once
and skip it straight away. A run now waits at most one bound.
Flux errors and the speak --model help said "Flux STT (listen v2)",
"Speak v2" and "Speak v1": API names nobody types. They now name the
model families (Flux models, flux-* and aura-* voices) and say what to
do instead.
The stdin banner said to send EOF with Ctrl+D, which doesn't end input
in a Windows console; Windows now gets "Ctrl+Z, then Enter". A refused
microphone printed PortAudio's text and macOS-only advice. It now uses
the catalogue's mic-denied message and hint, plus the setting's place
on macOS or Windows. Other microphone errors keep their own text.
Some tests that came from earlier releases measured wall-clock time
with bounds a loaded machine can miss. Each check keeps what it proves,
with a wider margin:

- skills: the lock wait is 2 s, so one wait (2 s) and one per tool
  (4 s) stay apart; a stale lock is taken well inside a 10 s timeout.
- config save: one 5 s lock wait is under 8.5 s; two would not be.
- FileInfo creation is measured in CPU time, 2 s for 1000 models.
- dg mcp e2e: no connection attempt within 12 s (one waits 15 s).
Ctrl-C and Ctrl-D now print "Cancelled." (exit 2), and an exception
nothing else handles prints "Command failed: <message>" with a next
step (exit 1). In agent mode both print exactly two prefixed lines, ERROR:
and HINT:, with the agent hint from the catalogue. The early
Ctrl-C path in main() prints the same lines without rich or Click.
The check reads the SigIgn mask from /proc/self/status, which exists only
on Linux, so on macOS and Windows it failed opening the file. It now
skips there and, on Linux, always checks the mask.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ting it

A plugin entry point named like a built-in command was refused before
its module was imported, but one named like a built-in alias, such as
debug-audio, was imported first and only refused once loaded. The check
before import now includes the top-level aliases too, so none of that
plugin's code runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dg login checks that the API base URL is trusted before it starts a
device login, but dg login --complete went straight to polling and
saving the approved key. It now runs the same check first, so an
untrusted DEEPGRAM_BASE_URL or --base-url stops it before any request,
and the pending login is kept for a retry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The speak tests had their own autouse fixture that cleared the agent
markers and DG_AGENT, which the root fixtures already clear for every
test. It is removed, and the remaining fixture's docstring covers the
agent output rules the speak tests relied on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments, docstrings and the manifest schema compared behavior with a
version number that was never released. The release they mean is 0.4.1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h uv

The deepctl-core and deepctl-cmd-debug-probe READMEs didn't list the
websockets requirement they now have. make readmes and make
readmes-check ran the system python3, which on some machines is too old
for the generator (it needs tomllib, Python 3.11+); they now use
uv run python, like the other Makefile targets, and the --check message
points at make readmes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/telemetry.md described error reports as sent by default. This
version has no built-in destination: the reporting code runs only with a
loopback DEEPCTL_TELEMETRY_DSN, for tests and local development. The
page now says so up front, and describes the switches, DEEPCTL_TELEMETRY=log
and the field schema as they apply to a loopback sink.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test removed a built-in's package by pointing its manifest entry at a
module that doesn't exist, but plugin_manager had already read the
built-in module table from the real manifest. The module then wasn't the
built-in's own, so the check that runs before import refused it as a
plugin replacing a built-in, and the test never reached the
dependency_missing error. That order is right: a module that isn't the
built-in's own is never imported. The test now patches both tables, as
an uninstalled package leaves them, and gets dependency_missing again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
python -m deepctl_cmd_mcp still parses the transport names an old MCP
config may use, streamable-http among them, so it can print the
stdio-only error instead of an argparse error. The lint that keeps
shipped text from promising other transports flagged that refusal. It now
skips that file for that one check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This release adds features and changes defaults that need upgrade notes,
so it is a minor release. With bump-patch-for-minor-pre-major set,
release-please would otherwise propose a patch release.

Release-As: 0.5.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dg-coreylweathers

Copy link
Copy Markdown
Contributor Author

Consolidated into #165, which has all 74 commits of 0.5.0 in one PR.

@dg-coreylweathers
dg-coreylweathers deleted the release/0.5.0-5-mcp-agent branch November 7, 2026 02:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant