Skip to content

fix: correct web command examples, document Flux TTS/STT, and honor -o json across account commands - #97

Merged
GregHolmes merged 5 commits into
mainfrom
chore/web-command-accuracy
Aug 18, 2026
Merged

fix: correct web command examples, document Flux TTS/STT, and honor -o json across account commands#97
GregHolmes merged 5 commits into
mainfrom
chore/web-command-accuracy

Conversation

@GregHolmes

@GregHolmes GregHolmes commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Split out from #96 deliberately: that PR was a verified output-neutral toolchain upgrade, this one changes content and fixes an output bug. #95 and #96 are now merged, so this sits on current main.

1. Seven advertised commands don't work 🔴

The ALL COMMANDS cards used a subcommand style the CLI has never had. Each was verified to fail:

Page said CLI response
dg keys create "ci-runner" No such command 'keys create'
dg projects list No such command 'projects list'
dg members list No such command 'members list'
dg models list --type stt No such command 'models list'
dg requests list --limit 20 -o json No such command 'requests list'
dg completion install --shell zsh No such command 'completion install'
dg api GET /v1/projects Got unexpected extra argument (/v1/projects)

These commands are flag-based. The page was already internally inconsistent — its FAQ text used the correct forms (dg projects --list, dg keys --create --comment, dg api /v1/projects --jq) while the cards used the broken style. Fixed to the verified forms.

2. Stale after the Flux changes (#89, #92)

The page said "Generate speech with Aura voices" with zero mentions of Flux, even though dg speak now defaults to flux-alexis-en. None of the new capabilities appeared anywhere (--speed, --expressivity, --redact, --numerals, flux-general-en all scored 0).

  • speak — Flux documented as the default engine, new cards for --speed / --expressivity, plus an explicit Aura card so -m aura-2-* stays discoverable
  • listen — new cards for --redact / --numerals and Flux STT streaming, noting it is streaming-only
  • hero terminal — a default dg speak now reports flux-alexis-en, not aura-2-asteria-en

3. Missing commands added

profiles, logout, ffprobe, debug browser, debug probe had no cards. 27 → 36 cards.

4. llms.txt / llms-full.txt

These feed AI crawlers and had the same staleness, plus a model name that doesn't exist (aura-2-en-us — confirmed rejected by the API, rc=2). Corrected to real ids and added the new flag rows.

5. -o json now honored across list/detail commands (fixed here)

While verifying, I found -o json was not honored consistently — several commands printed a Rich table or human text on stdout, ignoring the flag and corrupting anything piped into jq. This PR fixes it across requests, read, models, projects, members, usage, and billing (+ deepctl-core exports get_output_format):

  • Human tables/summaries are now gated on get_output_format() == "default"; for json/yaml/csv the framework serialises the returned result to stdout.
  • All status/progress chrome ("Fetching…", "Analyzing text…", errors) moved to a stderr Console, so stdout stays clean.
  • requests --show now returns a structured RequestDetailResult (the full request detail) instead of a bare "displayed" message, so json/yaml output is actually useful.

Verified live against the installed CLI + a real key: dg -o json models | jq, … projects --list, … requests --limit, and … read --sentiment all emit pure JSON on stdout with status text on stderr.

Tests

Added output-format gating tests for the three rendering shapes — table (requests, models), list (projects, new test file), and inline (read): each asserts that json mode writes nothing to the stdout console while still returning a fully-populated result, and that default mode still renders. Full suite green (make check + uv run pytest).

Verification

Every card invocation was run against the installed CLI (--help-level parse plus live runs for --redact pci --numerals, --speed/--expressivity, and every model id including a live-synthesis check that aura-2-en-us is rejected). Build passes; all seven stale forms confirmed absent from the built output and the new content confirmed present (including in the bundled hero-animation JS).

@GregHolmes
GregHolmes force-pushed the chore/web-command-accuracy branch from c816fa9 to 6e2f2ab Compare August 18, 2026 10:10
Base automatically changed from chore/web-astro-7 to main August 18, 2026 12:09
The ALL COMMANDS section advertised seven invocations that the CLI rejects.
Anyone copying them got an error:

  dg keys create "ci-runner"          -> No such command 'keys create'
  dg projects list                    -> No such command 'projects list'
  dg members list                     -> No such command 'members list'
  dg models list --type stt           -> No such command 'models list'
  dg requests list --limit 20 -o json -> No such command 'requests list'
  dg completion install --shell zsh   -> No such command 'completion install'
  dg api GET /v1/projects             -> Got unexpected extra argument

These commands are flag-based, not subcommand-based. The page was already
internally inconsistent: its FAQ used the correct forms (dg projects --list,
dg keys --create --comment) while the cards used a subcommand style that has
never existed.

Content was also stale after the Flux TTS default (#89) and SDK 7.7.0 (#92):
the page said "Generate speech with Aura voices" with no mention of Flux, and
none of the new flags appeared anywhere.

- speak: document Flux as the default engine plus --speed and --expressivity,
  and keep an explicit Aura card so -m aura-2-* stays discoverable
- listen: add --redact/--numerals and Flux STT streaming (flux-general-en),
  noting it is streaming-only
- hero terminal: a default `dg speak` now reports flux-alexis-en, not
  aura-2-asteria-en
- add the missing commands: profiles, logout, ffprobe, debug browser,
  debug probe (27 cards -> 36)
- llms.txt / llms-full.txt: same Flux corrections, add the new flag rows, and
  fix a model name that does not exist (aura-2-en-us -> real aura-2-* ids)

Every advertised invocation in the cards was validated against the installed
CLI; the shell-composed ones were checked by hand. Build passes and the stale
forms are absent from the built output.
@GregHolmes
GregHolmes force-pushed the chore/web-command-accuracy branch from 6e2f2ab to 52741d9 Compare August 18, 2026 12:11
…rs/usage/billing

These seven commands printed their Rich table and status chrome to stdout
unconditionally, so in -o json mode the framework's JSON was emitted *after*
the human output — corrupting stdout for anyone piping into jq. This makes the
web page's `dg -o json requests` example actually produce JSON.

Fix follows the existing house pattern (init/listen/speak): status/progress
chrome goes to a stderr Console, and the human table is gated on
get_output_format() == "default". The returned result (which the framework
serializes for json/yaml/csv) is unchanged, so default/interactive output is
identical. requests --show now returns the request detail in JSON instead of a
bare success message.

Also export get_output_format from deepctl_core.__all__ (it was importable but
not an explicit re-export; mypy flagged it).

Verified: all 7 commands emit pure jq-parseable JSON; default renders the
table; 60 unit tests pass; ruff and mypy clean across 21 source files.
@GregHolmes GregHolmes changed the title fix(web): correct 7 invalid command examples, document Flux TTS/STT, add missing commands fix: correct web command examples, document Flux TTS/STT, and honor -o json across account commands Aug 18, 2026
…format

- requests --show now returns RequestDetailResult (just `detail`) instead of
  RequestsResult, so single-request json/yaml output no longer emits an empty
  `requests: []` / `count: 0` beside the detail.
- Reword --speed in the web card + llms-full.txt to "in 0.05 steps from 0.85
  to 1.15" — the CLI accepts a discrete 7-value set, not a continuous range.
- Run `ruff format` on projects/requests/usage command.py so `make check`
  (the CI Lint gate) passes again.
Cover the three rendering shapes changed by the -o json fix — table
(requests, models), list (projects, new test file), and inline (read).
Each asserts that in json mode the command writes nothing to the stdout
console while still returning a fully-populated result, and that default
mode still renders. Also pins RequestDetailResult for `requests --show`
(no empty requests/count fields in the payload).
…stdout

Review follow-ups on #97:

- llms-full.txt: the `dg speak` flag table listed `--encoding` as
  (mp3, linear16, flac, wav, ogg). wav/ogg are `--container` values, not
  encodings, and Flux (the default engine) rejects everything but
  linear16/mulaw/alaw. Rewrote the `--encoding` row to match the CLI's
  own help (Aura vs Flux sets) and added a `--container` row. This file
  feeds AI crawlers, so the stale list actively taught broken commands.

- billing: in default mode the "Billing Period" header printed to the
  stderr status console while its table printed to stdout, so
  `dg billing --breakdown > out.txt` dropped the header. Route the header
  through the stdout console to match the table (json/yaml/csv still
  return early and render nothing). Added stream-routing tests.
@GregHolmes
GregHolmes merged commit 55984ec into main Aug 18, 2026
38 checks passed
@GregHolmes
GregHolmes deleted the chore/web-command-accuracy branch August 18, 2026 14:19
@github-actions github-actions Bot mentioned this pull request Aug 18, 2026
GregHolmes added a commit that referenced this pull request Aug 18, 2026
…oads

Review follow-ups on #101. Three defects that made "-o json keeps stdout
parseable" true only in the narrow case, plus the shared status console the
per-command copies were standing in for.

Exit codes. main.py caught SystemExit and dropped the code on the floor, so
every command exited 0 no matter what it reported -- including the auth-guard
failure that already raised SystemExit(1). A caller writing
`if dg -o json keys; then` took the success branch on a failed call, which
defeats the point of a parseable stdout. main.py now carries the code through
the post-run notifications and re-raises it, and BaseCommand maps a result
status to an exit code once, in one table: error -> 1, cancelled -> 130,
everything else -> 0.

Payload integrity. _output_yaml and _output_csv printed through Rich, which
treats square brackets as style markup and deletes them: an API key comment of
"[ci] runner" came out as "runner", silently, with no error. Rich also
hard-wrapped at the console width, injecting newlines into the middle of a csv
field. Both now go through _write_payload with markup, highlighting and
wrapping off. Still routed via `console` so --quiet keeps working. The JSON
path was already safe -- print_json escapes rather than interprets -- and is
left alone.

The unknown-format complaint moved to stderr, so it cannot corrupt the JSON we
fall back to.

get_status_console() gives core one definition of the stderr chrome console and
the seven commands fixed in #97 now use it. A per-command Console(stderr=True)
silently missed core's agentic no-color settings, and a new command reaching
for a bare Console() is exactly how keys regressed in the first place.
GregHolmes added a commit that referenced this pull request Aug 18, 2026
…sweep) (#101)

Last command in the `-o json` sweep started by #97, plus the review
follow-ups that came out of it.

#97 fixed **requests, read, models, projects, members, usage, billing**
— but `keys` wasn't in that sweep, so it was left as the one account
command whose stdout still broke pipes:

```console
$ dg -o json keys | jq -r '.keys[0].key_id'
jq: parse error: Invalid numeric literal at line 1, column 9
```

`Fetching API keys...` and the Rich table were going to stdout ahead of
the JSON.

## The keys fix — same pattern as the merged seven

- `status_console` for progress, errors and the empty-state notice, so
chrome never touches stdout
- Human rendering gated on `get_output_format() == "default"`: the list
table, created-key details, key details, and both dry-run summaries

The created key's **secret is unaffected** — it already travels in
`KeysResult.created_key.key`, so json/yaml/csv callers still receive it;
only the human echo is suppressed. There's a test asserting exactly
that.

## Review follow-ups

Reviewing the above turned up four more defects. All predate this
branch, but three of them sit in code it touches and all four make "`-o
json` keeps stdout parseable" true only in the narrow case, so they're
fixed here rather than deferred.

**`dg keys --create --dry-run` never ran.** `handle` read `project_id`
and `dry_run` with `.get()` and then forwarded `**kwargs` alongside
them, so every argument arrived twice and Python raised `got multiple
values for argument 'project_id'` before the body started — swallowed
into an error result, exit 0. The dry-run gating added here was
unreachable, and this PR's original "known limitation" note described
behaviour that path never had. Popping both fixes it; fixing only
`dry_run` just exposes the `project_id` collision behind it.

**Failures exited 0.** `main.py` caught `SystemExit` and discarded the
code, so no command could ever signal failure — including the auth-guard
failure that already raised `SystemExit(1)`. `if dg -o json keys; then`
took the success branch on a failed call. The code is now carried
through the post-run notifications and re-raised, and `BaseCommand` maps
status to exit code in one table, matching the contract already
published in `llms-full.txt` (`0 = success, 1 = error, 2 = user
interrupt`): `error` → 1, `cancelled` → 2, everything else → 0. Exiting
0 on failure violated that documented contract, so this restores
published behaviour rather than introducing a new one — no docs change
needed.

**`-o yaml` and `-o csv` silently deleted user data.** Both printed
through Rich, which treats square brackets as style markup and removes
them — a key comment of `[ci] runner` came out as `runner`, no error.
Rich also hard-wrapped at the console width, injecting newlines into the
middle of a csv field. Both now write the payload verbatim (markup,
highlighting and wrapping off), still via `console` so `--quiet` keeps
working. The JSON path was already safe (`print_json` escapes rather
than interprets) and is untouched.

**`dg keys --delete ID` without `--yes` deleted nothing and blamed the
user.** `BaseCommand.confirm` returns its default whenever any parameter
came from the command line, and `--delete KEY_ID` is itself such a
parameter — so the prompt was unreachable and the command always
returned `Cancelled by user` without asking or calling the API. It now
prompts on stderr when someone is there to answer (stderr, so `-o json`
stdout stays clean), and returns a usage error naming `--yes` when
nobody is.

Also: `get_status_console()` in core replaces the eight per-command
`Console(stderr=True)` declarations. A per-command console silently
missed core's agentic no-color settings, and a new command reaching for
a bare `Console()` is exactly how `keys` regressed in the first place —
this makes it correct by default.

## Verification

| check | result |
|---|---|
| `dg -o json keys \| jq -r '.keys[0].key_id'` | returns the id ✓ (was a
parse error) |
| all 8 commands under `-o json` | keys, projects, models, usage,
requests, members, billing, read — all valid JSON on stdout ✓ (models is
77KB, so the wrapping fix holds at size) |
| `-o yaml` / `-o csv` for keys | valid, and bracketed values now
survive verbatim ✓ |
| chrome under `-o json` | still on stderr ✓ |
| human output | unchanged under a pty ✓ |
| `keys --create --dry-run` | reports `dry_run`, calls nothing ✓ (was a
TypeError) |
| `keys --delete ID` without `--yes` | usage error naming `--yes`, exit
1, no API call ✓ |
| exit codes | error → 1, cancelled → 2,
success/dry_run/`--help`/`--version` → 0, unknown command → 2 ✓ —
matches documented contract |
| tests | **1089 passed** (22 new), `ruff check` and `mypy` clean |

## Still open in #98

**#98 stays open for one item:** the promise that stdout auto-switches
to JSON *when piped*. `dg keys | jq` still gets the Rich table, because
the auto-switch (`setup_output`) sits behind `is_agentic()`, which needs
3+ soft signals; a plain pipe from an interactive shell scores 1.
Lowering that threshold changes what `dg <anything> | less` does for
every command, so it's a product decision rather than a bug fix —
deliberately not made here.

## Note on #99

#99 fixed the stdout bug with a central format-aware console before #97
landed. Now that #97 is merged, #99 is redundant and would conflict, so
I've closed it. Its one good idea — a shared status-console primitive
instead of per-command declarations — is implemented here as
`get_status_console()`.
GregHolmes added a commit that referenced this pull request Aug 19, 2026
main()'s module-level `console` was a plain rich Console(), which writes to
stdout, and both of main()'s handlers print through it. So a crash, a bad
flag, an unknown command, or a bare `dg` wrote human-readable prose to
stdout -- `dg -o json not-a-command` put `Error: No such command ...` on
stdout and left stderr empty, so anything piping stdout into jq parsed the
error text instead of JSON.

This is the root-handler half of the #97 sweep. That issue's scope covered
moving errors to a stderr Console "so stdout stays clean", and #101 closed
the sweep, but both only reached the command layer; main()'s own handlers
were never moved. Reuse deepctl_core.output.stderr_console -- the same
console print_error() writes to, and the pattern deepctl-cmd-mcp already
follows -- so root-level and command-level diagnostics format identically,
including the no-color handling for agentic/CI callers.

Exit codes are unchanged (1 for errors, 2 for interrupt, 0 on success), and
success paths still write their payload to stdout. Adds tests asserting a
failing `dg -o json ...` writes nothing to stdout for both the unknown-
command and bad-flag paths, and that the cancellation notice is on stderr --
the assertion that would have caught this during the original sweep.
GregHolmes added a commit that referenced this pull request Aug 19, 2026
…t-code + error-stream correctness (#102)

Blocks #100 — merge this first, then let release-please regenerate the
release PR as **0.3.0**.

## What this fixes

**1. `dg update` on pip silently doesn't deliver the release (root
`pyproject.toml`).**
Root declared floors as low as `>=0.0.1`, and pip's default
`only-if-needed` upgrade strategy leaves any sub-package whose installed
version already satisfies its floor. Measured from published
`deepctl==0.2.26` with the release wheels available: only 4 of 17
released packages upgraded; `dg --version` reported the new number while
the keys fixes — including the `-o json` fix that headlines the release
— never arrived. (uv resolves fresh and is unaffected, so the same `dg
update` lands two users in different states.) Floors now match the
versions being published.

**2. Eight packages can import a symbol their declared core floor
doesn't guarantee (`packages/*/pyproject.toml`).**

`deepctl-cmd-{billing,keys,members,models,projects,read,requests,usage}`
import `get_status_console` (new in core 0.2.16) at module scope while
declaring `deepctl-core>=0.1.10`. PyPI's latest published core is
0.2.14, so `pip install --upgrade deepctl-cmd-keys` alone reproduces a
broken CLI: the command vanishes and the ImportError prints to stdout.
Same class of hand-bump as #92; release-please has no cross-package
dependency automation, so these floors are hand-maintained.

**3. A crash exited 2 — the code reserved for user interrupt
(`src/deepctl/main.py`).**
Per the published contract (0 = success, 1 = error, 2 = user interrupt),
`main()`'s generic exception handler now exits 1. Because `cli()` runs
with `standalone_mode=False`, Click usage errors (bad flag, unknown
command, bare `dg`) propagate to this same handler and move from 2 to 1
as well — consistent with the contract, which reserves 2 for interrupt.
Also repairs three tests whose `patch.object(cli, "__call__", ...)` was
inert (dunder lookup bypasses instance attributes) and adds a
usage-error exit-code test.

**4. …but that alone demoted a real Ctrl-C to 1
(`src/deepctl/main.py`).**
With `standalone_mode=False`, Click catches a `KeyboardInterrupt` raised
during command execution and re-raises it as `click.exceptions.Abort` —
a `RuntimeError` subclass, not a `KeyboardInterrupt`. In this repo the
path is more direct still: `BaseCommand` catches the interrupt itself
and raises `click.Abort()`. So a mid-command Ctrl-C — the common case —
bypassed the `KeyboardInterrupt` handler entirely and landed in the
generic handler that 3 just changed, exiting 1 and printing an empty
`Error: ` (`str(Abort())` is `""`). Before 3 that path exited 2
correctly by accident. `Abort` is now caught alongside
`KeyboardInterrupt`, so user cancellation (Ctrl-C, Ctrl-D at a prompt)
always exits 2, with a test on the `Abort` delivery path.

**5. Root diagnostics printed to stdout, corrupting `-o json`
(`src/deepctl/main.py`).**
`main()`'s module-level console was a plain rich `Console()`, which
writes to stdout, and both of its handlers print through it — so a
crash, bad flag, unknown command, or bare `dg` wrote human-readable
prose to stdout. `dg -o json not-a-command` put `Error: No such command
...` on **stdout** and left stderr empty, so anything piping stdout into
`jq` parsed the error text instead of JSON. This is the root-handler
half of the #97 sweep: that issue's scope covered moving errors to a
stderr console "so stdout stays clean", and #101 closed the sweep, but
both only ever reached the command layer. Now aliases
`deepctl_core.output.stderr_console` — the same console `print_error()`
writes to, and the pattern `deepctl-cmd-mcp` already follows — so
root-level and command-level diagnostics format identically, including
the no-color handling for agentic/CI callers. Exit codes are unchanged
and success paths still write their payload to stdout. Adds tests
asserting a failing `dg -o json ...` writes nothing to stdout
(unknown-command and bad-flag paths) and that the cancellation notice is
on stderr.

## Why the BREAKING CHANGE footer

The exit-code enforcement (#101) landed as `fix:`, so release-please
would ship it as patch 0.2.28 with no version signal — and #100
currently confirms that: root reads `0.2.28` and its diff contains no `⚠
BREAKING CHANGES` section at all. There is no machine-readable breaking
marker anywhere in the cycle; the behavior change exists only as
hand-written prose in `21b8333` / `e327a5e`. The break belongs to #101's
already-merged code, so it cannot come from a conventional-commit type
on this PR's own diff — it has to be injected where the version
arithmetic can see it. With `bump-minor-pre-major`, that makes root
**0.3.0**.

The signal is deliberately stated in three places, because which one
release-please actually reads depends on how this PR is merged:

- **`BREAKING CHANGE:` footer on `914e132`** — the primary. That commit
touches only `src/deepctl/main.py` and its tests, i.e. a root-only path,
so the break is attributed to root alone and the eight sub-packages stay
patch bumps. This is the one that survives a **merge commit**, and it is
the only variant that produces the intended release shape.
- **`fix!:` in the title** — insurance. This repo's recent PRs were
squash-merged, and with `squash_merge_commit_title: PR_TITLE` /
`squash_merge_commit_message: PR_BODY` a squash discards every commit
message, footer included. The `!` keeps root at 0.3.0 in that case.
- **`BREAKING CHANGE:` footer at the foot of this description** — so a
squash also carries the descriptive text into the `⚠ BREAKING CHANGES`
section rather than just the subject line.

Trade-off to know before merging: **prefer a merge commit.** A squash
collapses all five commits into one that touches root *and* the eight
`packages/deepctl-cmd-*/pyproject.toml` files, so the break gets
attributed to those eight paths too — they would take minor bumps (keys
0.0.3 → 0.1.0, usage 0.1.13 → 0.2.0, …) with a breaking-change entry
about CLI exit codes that has nothing to do with them, and root's floors
here (`>=0.0.4`) would then sit below what actually published,
re-seeding the staleness this PR exists to fix. Root reaches 0.3.0
either way; only the sub-package shape differs.

## After merge — steps on the regenerated #100

1. `uv lock` and commit (version bumps stale the lock; CI runs `uv sync
--locked` — this is the `eff5291` wall and recurs every release until
release.yml regenerates the lock itself).
2. Re-apply the behavior-change prose (`git show 21b8333 e327a5e`),
changing `0.2.26 to 0.2.28` → `0.2.26 to 0.3.0`, and fold in the
exit-code and output-stream details from this PR: crashes and usage
errors move 2 → 1, `2` stays reserved for user interrupt (Ctrl-C during
a command still exits 2), and root error/cancellation output moves from
stdout to stderr. Decide there whether the stream move gets its own `⚠
BREAKING CHANGES` line or reads as a plain fix — root lands on 0.3.0
either way, so it is a notes-wording call, not a version call.
3. Verify root reads 0.3.0 across manifest / `pyproject.toml` /
`__init__.py` / `CHANGELOG` heading, and the `⚠ BREAKING CHANGES`
section renders the footer text.

Verification here: full suite **1094 passed / 6 skipped**; `make check`
clean (ruff + mypy, 115 files); `uv lock --check` clean after both floor
commits; live probes — `--version` → 0, bare `dg` → 1, bad flag → 1,
unknown command → 1, mid-command Ctrl-C → 2; `dg -o json not-a-command`
writes **0 bytes** to stdout with the error on stderr, while `dg -o json
models` still emits valid JSON on stdout.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

BREAKING CHANGE: `dg` now exits non-zero when a command fails: 1 for
errors (including crashes and usage errors), 2 for user interrupt, 0 on
success. Every command previously exited 0 regardless of outcome, so
scripts and CI steps that ignored the exit code will surface failures
they
were silently swallowing. No command that succeeds changes its exit
code.
GregHolmes added a commit that referenced this pull request Aug 19, 2026
🤖 I have created a release *beep* *boop*
---


<details><summary>0.3.0</summary>

## [0.3.0](v0.2.27...v0.3.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** cap mcp &lt;2 (fixes broken dg mcp), commit uv.lock, require
twine &gt;=7 ([#95](#95))
([997cd36](997cd36))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **deps:** raise root dependency floors to the versions this release
publishes
([c0b0023](c0b0023))
* exit 1, not 2, when a command crashes or is misused
([914e132](914e132))
* keep exit 2 when Ctrl-C interrupts a running command
([b0e80e2](b0e80e2))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
* **release:** bump pypi-publish action to v1.14.2 for Metadata-Version
2.5 ([#94](#94))
([582cd83](582cd83))
* send root error and interrupt output to stderr, not stdout
([f4b7c48](f4b7c48))
* **web:** repair broken Heap snippet, upgrade astro 6→7, clear all 20
npm alerts ([#96](#96))
([11928fe](11928fe))


### Behavior changes

Alongside the exit-code change above, upgrading to 0.3.0 changes these:

* The full exit-code contract is now enforced end to end: `0` = success,
`1` = error, `2` = user interrupt. Crashes **and usage errors** (bad
flag, unknown command, bare `dg`) exit `1`; `2` is reserved for
cancellation, so Ctrl-C during a running command and Ctrl-D at a prompt
both still exit `2`.
* Error and cancellation messages are written to **stderr** instead of
stdout. `dg -o json …` therefore keeps stdout machine-readable when a
command fails — previously a failure printed `Error: …` prose to stdout,
so a script piping stdout into `jq` parsed the error text instead of
JSON. Successful commands still write their payload to stdout.
* `-o yaml` and `-o csv` no longer drop square-bracketed text from
values. Output was passed through a renderer that read `[...]` as style
markup and deleted it, so an API key comment of `[ci] runner` was
emitted as `runner`. Long values are also no longer hard-wrapped
mid-field.
* `dg keys --delete KEY_ID` now asks for confirmation on stderr instead
of always reporting `Cancelled by user` without deleting. In a
non-interactive context it exits `1` and tells you to pass `--yes`.
* `dg keys --create --dry-run` now reports what it would create. It
previously failed with an internal `TypeError`.

### Previously unreleased

0.2.27 was tagged on 2026-08-17 but never reached PyPI — its publish
step failed with `InvalidDistribution: Invalid distribution metadata:
'2.5' is not a valid metadata version`, which
[#94](#94) and
[#95](#95) then fixed. PyPI
therefore goes straight from 0.2.26 to 0.3.0, and this release is the
first published build to include the 0.2.27 changes:

* SDK 7.7.0 — Flux TTS controls, Flux STT fix, listen redact/numerals
([#92](#92))
([50d96cf](50d96cf))
* **speak:** default to Flux TTS (`flux-alexis-en`) instead of Aura 2
([#89](#89))
([5a0b698](5a0b698)).
This changes the default model for `dg speak`, so synthesised audio
differs unless you pass an `aura-*` model explicitly.
* **mcp:** swallow broken/closed-pipe on dg mcp startup notifications
and error path ([#88](#88))
([b24396e](b24396e))

Six packages tagged in that cycle also reach PyPI for the first time
here: `deepctl-cmd-listen` 0.0.14, `deepctl-cmd-login` 0.1.17,
`deepctl-cmd-skills` 0.0.7, `deepctl-cmd-speak` 0.0.4,
`deepctl-cmd-update` 0.2.6 and `deepctl-telemetry` 0.0.6.

Because 0.2.27 never published, `dg update` on pip also had to be
repaired for this release to arrive at all: root's inter-package
dependency floors were lower than the versions being published, so pip's
default `only-if-needed` strategy left most sub-packages stale and `dg
--version` reported the new number while the fixes never landed. Floors
now match the published versions exactly.
</details>

<details><summary>deepctl-core: 0.2.16</summary>

##
[0.2.16](deepctl-core-v0.2.15...deepctl-core-v0.2.16)
(2026-08-19)


### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))


### Behavior changes

* Commands now map their result status to a process exit code (`error` →
`1`, `cancelled` → `2`, otherwise `0`), and
`BaseCommand.exit_code_for()` exposes that mapping. Exit codes were
previously discarded, so every command exited `0`.
* `-o yaml` and `-o csv` payloads are written verbatim; the renderer no
longer interprets `[...]` as markup or wraps long values.
* New `get_status_console()` returns the shared stderr console for
status output. Commands should use it instead of declaring their own.
Packages that import it require `deepctl-core>=0.2.16`.
</details>

<details><summary>deepctl-cmd-projects: 0.2.0</summary>

##
[0.2.0](deepctl-cmd-projects-v0.1.13...deepctl-cmd-projects-v0.2.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

<details><summary>deepctl-cmd-usage: 0.2.0</summary>

##
[0.2.0](deepctl-cmd-usage-v0.1.13...deepctl-cmd-usage-v0.2.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

<details><summary>deepctl-cmd-mcp: 0.1.15</summary>

##
[0.1.15](deepctl-cmd-mcp-v0.1.14...deepctl-cmd-mcp-v0.1.15)
(2026-08-19)


### Bug Fixes

* **deps:** cap mcp &lt;2 (fixes broken dg mcp), commit uv.lock, require
twine &gt;=7 ([#95](#95))
([997cd36](997cd36))
</details>

<details><summary>deepctl-cmd-models: 0.1.0</summary>

##
[0.1.0](deepctl-cmd-models-v0.0.2...deepctl-cmd-models-v0.1.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

<details><summary>deepctl-cmd-keys: 0.1.0</summary>

##
[0.1.0](deepctl-cmd-keys-v0.0.3...deepctl-cmd-keys-v0.1.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

<details><summary>deepctl-cmd-read: 0.1.0</summary>

##
[0.1.0](deepctl-cmd-read-v0.0.2...deepctl-cmd-read-v0.1.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

<details><summary>deepctl-cmd-requests: 0.1.0</summary>

##
[0.1.0](deepctl-cmd-requests-v0.0.2...deepctl-cmd-requests-v0.1.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

<details><summary>deepctl-cmd-billing: 0.1.0</summary>

##
[0.1.0](deepctl-cmd-billing-v0.0.2...deepctl-cmd-billing-v0.1.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

<details><summary>deepctl-cmd-members: 0.1.0</summary>

##
[0.1.0](deepctl-cmd-members-v0.0.3...deepctl-cmd-members-v0.1.0)
(2026-08-19)


### ⚠ BREAKING CHANGES

* `dg` now exits non-zero when a command fails: 1 for errors (including
crashes and usage errors), 2 for user interrupt, 0 on success. Every
command previously exited 0 regardless of outcome, so scripts and CI
steps that ignored the exit code will surface failures they were
silently swallowing. No command that succeeds changes its exit code.

### Bug Fixes

* correct web command examples, document Flux TTS/STT, and honor -o json
across account commands
([#97](#97))
([55984ec](55984ec))
* dependency floors that let dg update skip this release, and exit-code
+ error-stream correctness
([#102](#102))
([fd1e8a4](fd1e8a4))
* **deps:** raise deepctl-core floor to 0.2.16 in the eight packages
that import get_status_console
([98f9e91](98f9e91))
* **keys:** honor -o json so stdout stays parseable (completes the
[#97](#97) sweep)
([#101](#101))
([e430a77](e430a77))
</details>

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
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