Repository navigation
feat(npm): make @deepgram/cli the real Deepgram CLI - #146
Conversation
Replaces `@lukeocodes`, who has left Deepgram, with the current DX owners: `@dg-coreylweathers`, `@GregHolmes` and `@deepgram-kiley`. All three are admins on this repo, so the rule takes effect on merge. Kept separate from #146 so that PR isn't waiting on a code owner who's no longer here.
GregHolmes
left a comment
There was a problem hiding this comment.
This PR makes @deepgram/cli a Node launcher for the Python CLI, adds npm/npx-aware update behavior, and publishes npm only after PyPI verification.
Requesting changes for two release-critical issues:
-
The next root release can still resolve published sibling packages that lack
deepctl_core.launcherandInstallMethod.NPM. Raise the sibling dependency floors on the generated release PR, add the plugin-to-update dependency, regenerateuv.lock, and enforce that work before PyPI publishing. -
On Windows, the launcher treats every cache root as trusted before executing cached
deepctl.exe. Validate ownership, ACLs, and reparse points, or disable cache reuse when equivalent validation is unavailable.
I also found first-run signal forwarding, stale llms version, and trailing-whitespace follow-ups.
Replies to Greg's review on #146installation.py:10 (sibling floors)Fixed in 64ed2cb. launcher.js:298 (Windows cache roots)Fixed in f130238. I went with your option: Windows never reuses a cache. No root is trusted, the launcher.js:625 (signals during setup)Fixed in 161dd87. Signal handlers go in before setup, setup is async (no spawnSync or sleepSync), and each setup step runs in its own session (setsid), so pip's children die with it. On a signal, the launcher removes the partial venv, releases the lock in .gitignore:18 (trailing CR)main's Stale llms version (review summary)
Also merged main, bumped engines to Node >=20, and added a Linux job that runs the launcher tests on Node 20.0.0, 20, 22 and 24. c1e6e87 |
c1e6e87 to
e90e1dd
Compare
Replies to Greg's review on #146Top-level noteHeads up: I squashed the branch to 4 commits on top of current main. The final code is identical to what you reviewed, there's no merge commit, and each commit builds and passes on its own:
installation.py:10 (sibling floors)Fixed in 3a5ea7d (the floors pins themselves are in dbd3a33). launcher.js:298 (Windows cache roots)Fixed in 4e8a7c0. I went with your option: Windows never reuses a cache. No root is trusted, the launcher.js:625 (signals during setup)Fixed in 4e8a7c0. Signal handlers go in before setup, setup is async (no spawnSync or sleepSync), and each setup step runs in its own session (setsid), so pip's children die with it. On a signal, the launcher removes the partial venv, releases the lock in .gitignore:18 (trailing CR)main's Stale llms version (review summary)
Also bumped engines to Node >=20 and added a Linux job that runs the launcher tests on Node 20.0.0, 20, 22 and 24. 3a5ea7d |
|
Thanks Greg. I've pushed seven commits on top; nothing you already reviewed was rewritten.
|
GregHolmes
left a comment
There was a problem hiding this comment.
This PR makes @deepgram/cli a Node launcher for the Python CLI and adds npm publishing after matching PyPI verification.
Requesting changes for three release-critical issues:
- the POSIX cache can trust a group-writable ancestor based only on matching numeric UID/GID;
- Windows still reuses an unverified uv cache;
- the release gate does not watch the npm package files it publishes.
I verified the earlier sibling-floor and cached-venv fixes, and ran 109 Node launcher tests plus 204 targeted Python tests in Docker; they pass. All GitHub checks are green, but the current tests encode these remaining unsafe paths rather than rejecting them.
7a7e08b to
ea0f37f
Compare
GregHolmes
left a comment
There was a problem hiding this comment.
Request changes
[B1] The production site can publish npm install instructions before npm serves this implementation. deploy-web and publish-npm both run after PyPI verification but do not wait for each other. The registry currently serves the old 0.4.0-beta.0 as latest, and a clean Node 20 npx @deepgram/cli --help fails with ETARGET. Add publish-npm to deploy-web.needs so the site deploys only after npm publication and its smoke test pass.
I also left inline comments for the public prerequisite copy and contributor release instructions.
Verified: PR CI is green; git diff --check passes; 111 Node launcher tests and 248 focused Python tests pass in Docker.
|
B1 and both S1s are in e6232e5 (deploy-web needs publish-npm, prereq copy fixed in README + llms-full). The older threads (Windows cache, signals, sibling floors, trailing CR, POSIX group-writable, Windows uv cache, release gate) were fixed earlier in the stack, so I resolved them. Re-requesting review. |
The @deepgram/cli npm package runs deepctl with DEEPCTL_LAUNCHER set. deepctl-core gains deepctl_core.launcher, which reads that variable once and drops it from the environment. deepctl-cmd-update reports the npm install method and gives npm-shaped update advice. deepctl-cmd-plugin installs plugins into its isolated venv, because the launcher's venv is replaced on every npm version bump. deepctl-cmd-skills shows its one-time prompt even when the launcher runs deepctl through uv. Those packages now need the new core API, so their floors move to deepctl-core>=0.3.0, and plugin declares deepctl-cmd-update>=0.2.7 (with a workspace source) for InstallMethod.NPM. uv.lock is regenerated.
A zero-dependency Node launcher (Node >=20) that runs deepctl at the npm package's own version: a cached private venv, then `uv tool run --from deepctl==<version>`, then a venv it builds under a lock, then a clear error. First-run setup is async and supervised. Signal handlers go in before setup starts, and each setup step runs in its own session. A signal kills the step, removes the partial venv, releases the lock and re-raises the signal. The lock's owner.json is always replaced atomically and records the running step's process group, so on Linux a run that breaks a stale lock can stop a pip left behind by a SIGKILLed launcher. Cached venvs are reused only from roots that pass an ownership check. Windows can't verify ACLs or reparse points, so it never reuses a cache: it runs through uv, or builds a one-run venv and deletes it afterwards. The README documents one known limitation: Node resets inherited SIG_IGN before any JS runs, so `nohup dg` and `trap '' INT` can't protect deepctl. .gitignore stops ignoring npm/lib, and .gitattributes accepts that file's CRLF line endings so `git diff --check` stays clean.
…n bumps release-please now bumps npm/package.json with the root version. After PyPI serves that exact version, a publish-npm job publishes the package through trusted publishing. CI runs the launcher's tests on Node 20.0.0, 20, 22 and 24, and a cross-OS smoke test runs on Node 22. check_dependency_floors.py gains SIBLING_FLOORS: update and skills pin deepctl-core, and plugin pins deepctl-cmd-update, to the workspace version, and --fix raises them. Rule 2 now evaluates whole specifiers. Floors only hold if every changed package is actually released. So check_release_bumps.py fails when a package's src/ or non-workspace dependencies changed since its last release tag without a version bump. On the release PR it reports as the release-bump-gate check: sync-release-pr posts it as a commit status on the bot's commits, and a test.yml job covers commits people push. It runs again in the build job before PyPI. verify-published installs from PyPI alone, then runs deepctl and imports the launcher.
The README and llms-full.txt document `npm install -g @deepgram/cli` and `npx @deepgram/cli`. llms-full.txt still said 0.2.4, so it moves to web/src/data/ and a prerendered endpoint now builds it with the version from pyproject.toml, the same way index.astro already reads it. The build fails if that version can't be read. The doc-command test follows the file to its new path.
Launcher failures printed as `dg: <message>` with the hint on a bare second line. They now print as `ERROR: <message>` and `HINT: <hint>`, the same two-line form the CLI uses, so a script or a person can tell the problem from the fix. That covers setup failures, an interrupted setup, a deepctl that can't start, and a deepctl that crashed.
…jobs The launcher and release jobs this branch adds pin actions/checkout to a commit whose tag is v7.0.1, but their comments said v4. They now name the tag the SHA points at, and the jobs that install uv pin its version (0.13.6) instead of taking the newest release.
The launcher used to install deepctl==<version> and let pip or uv resolve everything else from the index. Each release now ships requirements-<version>.txt in the npm package: deepctl and its workspace packages with the hashes of the files PyPI serves for them (read from its JSON API after the release is verified there), and every other dependency pinned with uv.lock's hashes. The release's publish-npm job writes it with npm/scripts/write_requirements.py, cross-checks it against the files that run built, and checks that the tarball contains it. Setup installs only from that file, with --require-hashes, --no-deps and --only-binary :all:, so a download that differs from the release is refused, nothing unlisted is installed, and no build backend runs. `uv tool run --with-requirements` ignores hashes, so with uv the launcher builds the same cached venv with `uv venv` and `uv pip install`, which check them. Setup runs in a directory the launcher owns, uv skips project configuration discovery (--no-config, with the user's or the machine's uv.toml passed back explicitly) and Python runs with -I, so running dg inside a repository can't change what gets installed, while pip.conf, uv.toml and PIP_*/UV_* mirror settings still apply. The pip fallback no longer upgrades pip first, since that download would be unhashed. Python is limited to 3.10 to 3.14, the versions the lock has wheels for. A package without its requirements file installs nothing. DEEPCTL_NPM_PYPI_SPEC, which CI uses to test against another deepctl, keeps the unhashed path. CI gains a Windows and macOS job that runs the hash-locked install from a local index (npm/scripts/local_index.py).
…rols A cache root is reused only when rootTrusted shows it is ours: a real directory, not a symlink, owned by this uid and not writable by group or others. When no root passes, the launcher used to stop with "No writable cache directory". It now builds a venv for that run in a fresh 0700 directory under the system temp directory, whose ancestors get the same ownership check, deletes it afterwards, and says on stderr which directory it would not trust and how to fix it. Nothing is written into the untrusted root. uv installs the files it extracted into its cache without hashing them again, and its default cache ($XDG_CACHE_HOME/uv) can sit in a directory others can write to. So uv always gets a launcher-owned cache: next to the venvs under the trusted root, inside the run's own directory (removed after setup, with pip running cacheless) when no root is trusted, and next to the one-run venvs on Windows. An existing cache directory that another user owns, or that group or others can write to, is not used: that run gets a one-run venv, and the hint says to remove the directory. A UV_CACHE_DIR the user set is left alone. Windows never reuses a venv. Its one-run venvs go under %LOCALAPPDATA%\deepgram-cli\venvs, and the directories the launcher creates there must be plain directories, not symlinks or junctions, before anything is built in them.
deepctl_core.launcher becomes deepctl_core._launcher, with no public re-export, so plugins don't build on it. deepctl-cmd-update, deepctl-cmd-skills, deepctl.main and the release's post-publish import check use the new name. capture_launcher() now takes the value from deepctl.main when that module has already captured DEEPCTL_LAUNCHER at startup (LAUNCHER_CAPTURED), and reads and removes the variable itself otherwise, so an entry point that captures it first keeps working.
Run the two real-filesystem tests for the Linux cache path only on Linux and macOS, as the other real-filesystem tests already are, and compare package paths against the realpath of the temp directory, which macOS reports under /private/var. Add a Windows test with a short-name (8.3) LOCALAPPDATA to show only link and junction flags decide whether a directory is safe.
… tests The launcher now tries python3.14 first, but the end-to-end and signal tests only installed fake python3.13 to python3 executables. On a machine with a real python3.14 on PATH, such as GitHub's ubuntu runners, the launcher found that one instead, built a real venv and ran a real pip, so those tests failed or timed out waiting for the fake step. The fakes now cover every name the launcher probes, from one list. Regular-expression escaping in the tests also goes through one helper that escapes backslashes and every other special character.
A group id equal to our uid (the user-private-group convention) does not prove the group has no other members: an administrator can add one, and Node cannot list them reliably. Any ancestor writable by group or others now has to be sticky, like /tmp. A umask-002 ~/.cache (0775) therefore fails, and the venvs go to the $TMPDIR/deepgram-cli-<uid> root, which is held to the same checks and still caches across runs. The npm README says where the cache goes in that case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
uv installs files it extracted into its cache without hashing them again, so a cache that outlives the run has to be as trusted as a venv. Windows cannot check owners, ACLs or reparse points from Node, so it no longer keeps %LOCALAPPDATA%\deepgram-cli\uv-cache: uv gets a cache inside the run's own directory, removed once setup is done. pip keeps its own cache, since with --require-hashes it hashes every file it installs. A UV_CACHE_DIR in the environment no longer switches the launcher's cache off: tools set it without the user choosing to (astral-sh/setup-uv exports it on GitHub's runners), so the launcher always passes --cache-dir, which uv puts ahead of UV_CACHE_DIR. The hash-locked Windows CI job plants a uv cache where the launcher used to keep it and fails if uv writes to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release bump gate watched only each package's src/. The root package also publishes @deepgram/cli, so a chore: or refactor: to npm/lib/ or npm/bin/ passed without a release. The root now also watches every entry in npm/package.json's `files`, read from HEAD so new entries are covered, with a glob matching at any depth as npm does, and package.json itself. npm/README.md and npm/LICENSE stay out, like the packages' READMEs: a docs change should not force a release. pyproject.toml is now compared on the metadata that reaches the wheel, not just dependencies: the [project] table without its version, [build-system], [tool.setuptools], and [tool.hatch]'s build and metadata tables. An entry point or requires-python change without a bump is caught too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e6232e5 to
17d2364
Compare
…nd docs - docs: note that npm/npx fail on Intel Macs and Windows on ARM64 (cryptography has no wheel there) and point Intel Macs to Homebrew; correct the "Python 3.10+ required" claim in llms-full.txt - npm/README: add an Uninstall section, cover project-local installs, and fix the cache fallback note (applies on macOS too) - update: give the project-local npm update command alongside the global one - core: document why capture_launcher() honours deepctl.main's LAUNCHER_CAPTURED hand-off (needed once deepctl.main is stdlib-only) - tests: skip release-bump tests that need git or a git checkout when unavailable instead of failing with FileNotFoundError - ci: pin build, twine and packaging to the uv.lock versions
🤖 I have created a release *beep* *boop* --- <details><summary>0.4.1</summary> ## [0.4.1](v0.4.0...v0.4.1) (2026-11-12) ### Features * **cli:** detect npm launcher installs in update, plugin and skills ([07e6fd9](07e6fd9)) * **npm:** add the @deepgram/cli launcher package ([a842404](a842404)) * **npm:** make @deepgram/cli the real Deepgram CLI ([#146](#146)) ([8ed20ee](8ed20ee)) ### Bug Fixes * **audio:** stop reading a file name that starts with - as ffprobe options ([184c3ec](184c3ec)) * **auth:** honor DEEPGRAM_CLI_INSECURE only for 1/true and localhost ([0da2fa0](0da2fa0)) * **auth:** send keys and data only to the host you chose ([945338b](945338b)) * **cli:** escape characters the output encoding can't write ([17cac1e](17cac1e)) * **cli:** keep ffprobe path and plugin changes out of AI agent sessions ([2df96f0](2df96f0)) * **cli:** stop printing local variables in crash tracebacks ([7098b24](7098b24)) * **cli:** stop splitting hyphenated file names like debug-call.wav into a command ([430706e](430706e)) * **config:** limit what a project deepgram.yaml or a non-default --config file can set ([a71a918](a71a918)) * **config:** read and write the config file safely ([36805f2](36805f2)) * **config:** report a config file that can't be looked up instead of crashing ([cd11fe8](cd11fe8)) * **config:** treat OpenCode as an AI agent and never reuse a refused trust root ([c7d9e35](c7d9e35)) * **consent:** load Ketch first and hold Heap and GA4 until consent ([6311875](6311875)) * **core:** install deepgram-skills-v1.8.0 by default ([77871b5](77871b5)) * **debug:** accept probe connections only from local pages ([3e10c7a](3e10c7a)) * **ffprobe:** never run an ffprobe found in the current directory or a relative PATH entry ([37d56c6](37d56c6)) * **init:** stop reading a template URL that starts with - as a git option ([e76bfe1](e76bfe1)) * **init:** write .env owner-only and add it to .gitignore ([e5a5c70](e5a5c70)) * **install:** verify a pinned uv installer before running it ([ff17cc7](ff17cc7)) * **mcp:** remove the SSE transport; dg mcp runs over stdio only ([ffaa148](ffaa148)) * **mcp:** serve only Deepgram docs search, and connect only when a tool is called ([9090a80](9090a80)) * **npm:** address devrel-review findings on platform support, tests and docs ([1eb7b64](1eb7b64)) * **npm:** install the CLI only from hash-locked requirements ([d28e851](d28e851)) * **npm:** keep venvs and uv's cache in directories only the user controls ([8ce3461](8ce3461)) * **npm:** never reuse a uv cache the launcher can't check ([9202b34](9202b34)) * **npm:** never trust a group-writable cache ancestor ([a8ee1e3](a8ee1e3)) * **npm:** print launcher errors as ERROR and HINT lines ([a88b434](a88b434)) * **plugin:** keep plugins from replacing built-in commands ([dff78a2](dff78a2)) * **plugin:** make the plugin folders private ([6b9c257](6b9c257)) * **telemetry:** stop sending error reports ([70033dd](70033dd)) * **update:** upgrade the deepgram Homebrew formula, not deepctl ([f41226f](f41226f)) * **web:** self-host Inter so no font request precedes consent ([5cd0725](5cd0725)) ### Documentation * document the npm install ([85df8bb](85df8bb)) * give the real order of settings and of the API key ([e8772ac](e8772ac)) * **npm:** fix a dangling reference in the Intel Mac brew note ([d28c7a5](d28c7a5)) * tell plugin authors not to name an entry point like a built-in command ([371fb7a](371fb7a)) * **web:** show a plugin Deepgram publishes in the plugin example ([09adc52](09adc52)) </details> <details><summary>deepctl-core: 0.3.1</summary> ## [0.3.1](deepctl-core-v0.3.0...deepctl-core-v0.3.1) (2026-11-12) ### Features * **cli:** detect npm launcher installs in update, plugin and skills ([07e6fd9](07e6fd9)) * **npm:** make @deepgram/cli the real Deepgram CLI ([#146](#146)) ([8ed20ee](8ed20ee)) ### Bug Fixes * **auth:** honor DEEPGRAM_CLI_INSECURE only for 1/true and localhost ([0da2fa0](0da2fa0)) * **auth:** send keys and data only to the host you chose ([945338b](945338b)) * **cli:** keep ffprobe path and plugin changes out of AI agent sessions ([2df96f0](2df96f0)) * **config:** limit what a project deepgram.yaml or a non-default --config file can set ([a71a918](a71a918)) * **config:** read and write the config file safely ([36805f2](36805f2)) * **config:** report a config file that can't be looked up instead of crashing ([cd11fe8](cd11fe8)) * **config:** treat OpenCode as an AI agent and never reuse a refused trust root ([c7d9e35](c7d9e35)) * **core:** install deepgram-skills-v1.8.0 by default ([77871b5](77871b5)) * **ffprobe:** never run an ffprobe found in the current directory or a relative PATH entry ([37d56c6](37d56c6)) * **npm:** address devrel-review findings on platform support, tests and docs ([1eb7b64](1eb7b64)) * **plugin:** keep plugins from replacing built-in commands ([dff78a2](dff78a2)) * **plugin:** make the plugin folders private ([6b9c257](6b9c257)) * **telemetry:** stop sending error reports ([70033dd](70033dd)) </details> <details><summary>deepctl-shared-utils: 0.1.14</summary> ## [0.1.14](deepctl-shared-utils-v0.1.13...deepctl-shared-utils-v0.1.14) (2026-11-12) ### Bug Fixes * **audio:** stop reading a file name that starts with - as ffprobe options ([184c3ec](184c3ec)) * **ffprobe:** never run an ffprobe found in the current directory or a relative PATH entry ([37d56c6](37d56c6)) </details> <details><summary>deepctl-telemetry: 0.0.8</summary> ## [0.0.8](deepctl-telemetry-v0.0.7...deepctl-telemetry-v0.0.8) (2026-11-12) ### Bug Fixes * **config:** limit what a project deepgram.yaml or a non-default --config file can set ([a71a918](a71a918)) * **config:** read and write the config file safely ([36805f2](36805f2)) * **telemetry:** stop sending error reports ([70033dd](70033dd)) </details> <details><summary>deepctl-cmd-api: 0.0.3</summary> ## [0.0.3](deepctl-cmd-api-v0.0.2...deepctl-cmd-api-v0.0.3) (2026-11-12) ### Bug Fixes * **auth:** send keys and data only to the host you chose ([945338b](945338b)) </details> <details><summary>deepctl-cmd-login: 0.2.1</summary> ## [0.2.1](deepctl-cmd-login-v0.2.0...deepctl-cmd-login-v0.2.1) (2026-11-12) ### Bug Fixes * **auth:** send keys and data only to the host you chose ([945338b](945338b)) </details> <details><summary>deepctl-cmd-projects: 0.2.1</summary> ## [0.2.1](deepctl-cmd-projects-v0.2.0...deepctl-cmd-projects-v0.2.1) (2026-11-12) ### Bug Fixes * **auth:** send keys and data only to the host you chose ([945338b](945338b)) * **config:** read and write the config file safely ([36805f2](36805f2)) </details> <details><summary>deepctl-cmd-debug-audio: 0.1.15</summary> ## [0.1.15](deepctl-cmd-debug-audio-v0.1.14...deepctl-cmd-debug-audio-v0.1.15) (2026-11-12) ### Bug Fixes * **audio:** stop reading a file name that starts with - as ffprobe options ([184c3ec](184c3ec)) * **ffprobe:** never run an ffprobe found in the current directory or a relative PATH entry ([37d56c6](37d56c6)) </details> <details><summary>deepctl-cmd-debug-probe: 0.0.3</summary> ## [0.0.3](deepctl-cmd-debug-probe-v0.0.2...deepctl-cmd-debug-probe-v0.0.3) (2026-11-12) ### Bug Fixes * **auth:** send keys and data only to the host you chose ([945338b](945338b)) * **debug:** accept probe connections only from local pages ([3e10c7a](3e10c7a)) </details> <details><summary>deepctl-cmd-ffprobe: 0.0.4</summary> ## [0.0.4](deepctl-cmd-ffprobe-v0.0.3...deepctl-cmd-ffprobe-v0.0.4) (2026-11-12) ### Bug Fixes * **cli:** keep ffprobe path and plugin changes out of AI agent sessions ([2df96f0](2df96f0)) * **ffprobe:** never run an ffprobe found in the current directory or a relative PATH entry ([37d56c6](37d56c6)) </details> <details><summary>deepctl-cmd-mcp: 0.1.16</summary> ## [0.1.16](deepctl-cmd-mcp-v0.1.15...deepctl-cmd-mcp-v0.1.16) (2026-11-12) ### Bug Fixes * **auth:** send keys and data only to the host you chose ([945338b](945338b)) * **mcp:** remove the SSE transport; dg mcp runs over stdio only ([ffaa148](ffaa148)) * **mcp:** serve only Deepgram docs search, and connect only when a tool is called ([9090a80](9090a80)) * **telemetry:** stop sending error reports ([70033dd](70033dd)) </details> <details><summary>deepctl-cmd-update: 0.2.8</summary> ## [0.2.8](deepctl-cmd-update-v0.2.7...deepctl-cmd-update-v0.2.8) (2026-11-12) ### Features * **cli:** detect npm launcher installs in update, plugin and skills ([07e6fd9](07e6fd9)) * **npm:** make @deepgram/cli the real Deepgram CLI ([#146](#146)) ([8ed20ee](8ed20ee)) ### Bug Fixes * **config:** read and write the config file safely ([36805f2](36805f2)) * **npm:** address devrel-review findings on platform support, tests and docs ([1eb7b64](1eb7b64)) * **update:** upgrade the deepgram Homebrew formula, not deepctl ([f41226f](f41226f)) </details> <details><summary>deepctl-cmd-plugin: 0.1.15</summary> ## [0.1.15](deepctl-cmd-plugin-v0.1.14...deepctl-cmd-plugin-v0.1.15) (2026-11-12) ### Features * **cli:** detect npm launcher installs in update, plugin and skills ([07e6fd9](07e6fd9)) * **npm:** make @deepgram/cli the real Deepgram CLI ([#146](#146)) ([8ed20ee](8ed20ee)) ### Bug Fixes * **cli:** keep ffprobe path and plugin changes out of AI agent sessions ([2df96f0](2df96f0)) * **plugin:** keep plugins from replacing built-in commands ([dff78a2](dff78a2)) * **plugin:** make the plugin folders private ([6b9c257](6b9c257)) </details> <details><summary>deepctl-cmd-skills: 0.0.10</summary> ## [0.0.10](deepctl-cmd-skills-v0.0.9...deepctl-cmd-skills-v0.0.10) (2026-11-12) ### Features * **cli:** detect npm launcher installs in update, plugin and skills ([07e6fd9](07e6fd9)) * **npm:** make @deepgram/cli the real Deepgram CLI ([#146](#146)) ([8ed20ee](8ed20ee)) </details> <details><summary>deepctl-cmd-init: 0.1.1</summary> ## [0.1.1](deepctl-cmd-init-v0.1.0...deepctl-cmd-init-v0.1.1) (2026-11-12) ### Bug Fixes * **init:** stop reading a template URL that starts with - as a git option ([e76bfe1](e76bfe1)) * **init:** write .env owner-only and add it to .gitignore ([e5a5c70](e5a5c70)) </details> <details><summary>deepctl-cmd-speak: 0.0.6</summary> ## [0.0.6](deepctl-cmd-speak-v0.0.5...deepctl-cmd-speak-v0.0.6) (2026-11-12) ### Bug Fixes * **auth:** send keys and data only to the host you chose ([945338b](945338b)) </details> <details><summary>deepctl-cmd-listen: 0.0.15</summary> ## [0.0.15](deepctl-cmd-listen-v0.0.14...deepctl-cmd-listen-v0.0.15) (2026-11-12) ### Bug Fixes * **auth:** send keys and data only to the host you chose ([945338b](945338b)) </details> <details><summary>deepctl-plugin-example: 0.1.13</summary> ## [0.1.13](deepctl-plugin-example-v0.1.12...deepctl-plugin-example-v0.1.13) (2026-11-12) ### Documentation * tell plugin authors not to name an entry point like a built-in command ([371fb7a](371fb7a)) </details> --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Bring the 0.4.1 release into 0.5.0: the @deepgram/cli npm launcher (#146), the Homebrew bump that merges itself (#135), and the 0.4.1 fixes (#159). How the two sides fit: - src/deepctl/main.py is 0.5.0's. Its main() reads and removes DEEPCTL_LAUNCHER before deepctl_core is imported and keeps the value in LAUNCHER / LAUNCHER_CAPTURED, which deepctl_core._launcher (from 0.4.1) and deepctl_cmd_update.channel both read. Crash tracebacks still never show frame locals (install(show_locals=False), now installed lazily). - deepctl-cmd-update is 0.5.0's, which already detects npm and npx installs; 0.4.1's duplicate detection there is dropped. The plugin command keeps 0.4.1's isolated venv for npm installs, and the skills prompt keeps 0.4.1's rule that an npm launch is not one-shot. - Versions are lockstep at 0.4.1 (what main last released); the plugin example stays at its released 0.1.13. release-please now also moves npm/package.json with the root version. - scripts/check_release_bumps.py and its release-bump-gate are removed: with lockstep versions every release bumps every package, so no package's code can reach PyPI under an already-published version. The build job checks lockstep versions once more before publishing. - release.yml keeps 0.5.0's re-pin jobs, trusted publishing and tag recovery, and adds 0.4.1's pinned build tools, fresh-venv PyPI check, Homebrew auto-merge and publish-npm. test.yml adds the three npm launcher jobs to All checks. - conftest.py forgets the launcher main() captured between tests, so a test that ran main() no longer decides what later launcher tests see.
Why
@deepgram/clion npm is the obvious name for "the Deepgram CLI", and it's broken:npm i @deepgram/clifails withnotarget. The only versions are two old betas (0.3.0-beta.0,0.4.0-beta.0) fromlukeocodes/deepgram-cli.npm i @deepgram/cli@0.4.0-beta.0installs, then crashes on start:MODULE_NOT_FOUND …/src/hooks/init/log.Coding agents try npm first. In our build-journey evals, agents asked to "use the Deepgram CLI" went to
@deepgram/cli, hit both failures, and gave up on the CLI.This PR makes
@deepgram/clithe real CLI.What it does
New
npm/package.@deepgram/clihas no dependencies and no install scripts. It providesdganddeepgram, and runs the Pythondeepctlat the same version as the npm package:~/.cache/deepgram-cli/venvs/<V>) if one is already built. This is fast and works offline.uv pip install --require-hashes --no-deps --only-binary :all:against the hash-pinnedrequirements-<V>.txtthe package ships. No local Python is needed.venvbuilds the same hash-checked venv the same way, in place under a lock. All bootstrap output goes to stderr, sodg -o jsonanddg mcpstdout stay clean on the first run.python3-venv, or useinstall.sh/brew.DEEPCTL_NPM_RUNNER=uvx|venv|pathforces a strategy.pathuses adeepctlalready on PATH, and only when asked.Python side.
InstallMethod.NPMis detected throughDEEPCTL_LAUNCHER, whichdeepctl_core._launcherreads once and removes from the environment so it doesn't leak to child processes. With it:dg updateand the update banner tell npm and npx users the npm command.dg.Release.
npm/package.jsonalong with the root package (extra-files).publish-npmjob runs afterverify-published, so npm never points at a version PyPI can't install.npm-publish, npm pinned to 11.21.0) and sets provenance.latest, it publishes underv<maj>.<min>-maintinstead of movinglatest.CI. Three new jobs, added to
all-checks:launcher(Linux, macOS, Windows): Node unit tests,cmp LICENSE npm/LICENSE, and a check that stdin stays blocking for deepctl.launcher-node(Linux): Node unit tests on every versionpackage.json'senginesadmits (20.0.0, 20, 22, 24).launcher-hashed(Windows, macOS): the release path end to end against a local package index — builds this commit's packages, writes the hash-lockedrequirements-<V>.txt, and runs the launcher through both uv and the pip fallback.release-bump-gatestatus (scripts/check_release_bumps.py) fails the build if a package's shipped code changed since its last release tag without a version bump — the drift that would otherwise let a sibling package ship against adeepctl-corePyPI hasn't released yet. It runs again before publish with the same check.Docs. npm/npx install lines in
README.mdandweb/src/data/llms-full.txt. The latter also fixes thedeepgram/tap/deepctl→deepgram/tap/deepgramtypo.The first npm version will be 0.4.1, from the next release PR.
0.4.1is newer than0.4.0-beta.0, solatestmoves and plainnpm i @deepgram/cliworks.Hardening
This went through five rounds of independent review, each followed by a fix round. Every finding was reproduced in Docker before it was fixed, and each fix was checked against its repro. Highlights:
Cache security.
gid == uid); a shared-group cache falls back to a private temp dir./tmpfallback is0700and owner-checked.These checks block another user from planting or swapping a cached venv. Tested by pre-creating the cache, with a symlink, and with a 150-iteration swap loop.
Locking.
mkdirlock with owner, pid and host.Signals and stdio.
dg mcpover stdio,ffmpeg | dg listen, and similar pipes.Self-repair and pruning.
Testing
Docker (linux/arm64; the uvx, venv, signal and stdin cases also on amd64): 43/43 cases pass. They cover:
python3-venv(PEP 668);npx ./tgz;initializesent 1s late.Unit and lint:
enginesfloor).check_dependency_floors.pyand actionlint are clean.test_release_bumps.pytests that read this repository's git history now skip when git or a git checkout isn't available, instead of failing withFileNotFoundError. CI has both, so they still run there.pip installsteps inrelease.ymland therelease-bump-gatejob are pinned to the versions inuv.lock(build==1.5.0,twine==7.0.0,packaging==26.3).Not yet run: real macOS and Windows runs, which the new
launcherjob does on this PR, and thepublish-npmjob, which runs on the first release now that the owner setup below is in place.Before the first release, owner steps (done 2026-10-23)
@deepgram/cli(set asdeepgramai): GitHub Actions, repodeepgram/cli, workflowrelease.yml, environmentnpm-publish, permission publish. Note for anyone redoing this: npm now separatespublishfromstage publish; the first attempt from the web UI granted onlystage publish, which a plainnpm publishis rejected under, so it was revoked and re-added withnpm trust github @deepgram/cli --repo deepgram/cli --file release.yml --env npm-publish --allow-publish. Verify withnpm trust list @deepgram/cli.npm-publishcreated on this repo, deployment branches limited tomain, no required reviewer.Unsupported 2023 beta. Install the Deepgram CLI with: npm i -g @deepgram/cli@latest (0.4.1 or newer). (npm Support had deprecated them on 2026-09-11 with its generic "no longer supported" text; the flag is per version and does not block publishing.)npm view @deepgram/cli dist-tags.latestis0.4.1, then turn on "Require 2FA and disallow tokens" for the package.Until the first publish,
npx @deepgram/clistill runs the old 2023 beta (now with the pointer message), so the README line goes live slightly ahead of the package.Floors
deepctl-cmd-updateanddeepctl-cmd-skillsalready declaredeepctl-core>=0.3.0on this branch, anddeepctl-cmd-pluginalready declaresdeepctl-cmd-update>=0.2.7for theInstallMethod.NPMimport it's always made without declaring. No hand edits are needed on the release PR: the release sync job (check_dependency_floors.py --fix) keeps these floors current automatically, andrelease-bump-gate(above) catches it if a future change needs a bump this job doesn't cover.A normal
pip install -U deepctlis already safe, because the root floors pin everything.Known limits and follow-ups (not in this PR)
Accepted limits:
pip install deepctlinto the same prefix overwrites npm'sdg, and brew and npm both wantdg. The npm README says to use one install method.staff, SUSEusers) fall back to a private temp dir. Any user-private-group member could interfere, which is a limit of that convention.nohup dg …doesn't survive SIGHUP the waynohup deepctl …does, because of how Node handles it. Usesetsidor deepctl directly for detached runs.--only-binary :all:, andcryptography50.0.0 has no wheel for either platform.README.md,npm/README.mdandllms-full.txtsay so. On an Intel Mac they point to Homebrew, because the tap formula brings the Rust toolchain that a source build ofcryptographyneeds.DEEPCTL_LAUNCHER=npmfor both a global install and a project dependency, because it has no reliable, portable way to tell them apart.dg updateshows both the global and the project-local command (npm install @deepgram/cli@latest); the update notice shows only the global one.npm/README.mdcovers both and has a new Uninstall section.Notes for reviewers:
deepctl_core._launcher.capture_launcher()still checksdeepctl.main.LAUNCHER_CAPTUREDfirst. It can look like dead code on this branch, but it's kept on purpose. In 0.5.0 (Release 0.5.0: faster startup, agent mode, streaming and one transport #165),deepctl.mainbecomes stdlib-only and removesDEEPCTL_LAUNCHERbeforedeepctl_coreis imported, so that flag is the only way npm installs are still detected. The docstrings now explain this.publish-on-release.yml(line 46) also runs an unpinnedpip install build twine. That file isn't changed by this PR, so it's left for a follow-up.Existing bugs found along the way:
dg updatetells Homebrew users to runbrew upgrade deepctl, but the formula isdeepgram.PermissionErroron~/.deepctl/plugins/venv).actions/checkoutpins are commented# v4but resolve to v7.0.1.Phase 2: PyInstaller per-platform binaries through
optionalDependencies, so npm installs need no Python or uv at all.The history includes a manual re-publish path that was added and later removed, so squash on merge.