From da962f980d94ebd7e2f4559edb5f09451da7782d Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 16 Jan 2026 01:12:36 -0800 Subject: [PATCH 1/5] feat(nix): add flake with dev shell and NixOS module Add Nix flake providing: 1. Dev shell with system dependencies - uv + Python 3.12 for package management - portaudio, ffmpeg, sox for audio - Works on Linux and macOS 2. NixOS module for AI services - services.agent-cli.ollama.{enable, acceleration, host, ...} - services.agent-cli.whisper.{enable, model, language, device, uri} - services.agent-cli.piper.{enable, voice, uri} - services.agent-cli.openwakeword.{enable, preloadModels, uri} Install agent-cli via uv (Python deps not packaged in nixpkgs). Remove shell.nix (replaced by flake devShell). --- docs/installation/flake.md | 223 +++++++++++++++++++++++++++++++++++++ flake.lock | 27 +++++ flake.nix | 215 +++++++++++++++++++++++++++++++++++ shell.nix | 16 --- 4 files changed, 465 insertions(+), 16 deletions(-) create mode 100644 docs/installation/flake.md create mode 100644 flake.lock create mode 100644 flake.nix delete mode 100644 shell.nix diff --git a/docs/installation/flake.md b/docs/installation/flake.md new file mode 100644 index 000000000..3a5f5d318 --- /dev/null +++ b/docs/installation/flake.md @@ -0,0 +1,223 @@ +# Nix Flake for NixOS + +Nix flake providing a development shell and NixOS module for AI service configuration. + +## Prerequisites + +- NixOS with flakes enabled +- 8GB+ RAM (16GB+ recommended for GPU) + +### Enable Flakes + +Add to `/etc/nix/nix.conf`: +``` +experimental-features = nix-command flakes +``` + +## Quick Start + +### Development Shell + +```bash +git clone https://github.com/basnijholt/agent-cli.git +cd agent-cli +nix develop + +# Inside the shell: +uv sync --all-extras +uv run agent-cli --help +``` + +The dev shell provides: +- Python 3.12 with uv for package management +- System dependencies (portaudio, ffmpeg, sox) +- Build tools (gcc, pkg-config) + +### Install agent-cli + +```bash +# Using uv (recommended) +uv tool install agent-cli + +# Or with pip +pip install agent-cli +``` + +## NixOS Module + +The flake includes a NixOS module that configures AI backend services (Ollama, Wyoming Whisper/Piper/OpenWakeWord). + +### Basic Usage + +Add to your NixOS flake: + +```nix +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + agent-cli.url = "github:basnijholt/agent-cli"; + }; + + outputs = { self, nixpkgs, agent-cli, ... }: { + nixosConfigurations.yourhostname = nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + agent-cli.nixosModules.default + { + services.agent-cli = { + enable = true; + # All services enabled by default + }; + } + ]; + }; + }; +} +``` + +Then rebuild: +```bash +sudo nixos-rebuild switch --flake .#yourhostname +``` + +### Module Options + +```nix +services.agent-cli = { + enable = true; # Enable AI backend services + + # Ollama LLM + ollama = { + enable = true; # Default: true + acceleration = "cuda"; # "cuda", "rocm", or "cpu" + host = "127.0.0.1"; # Host address + environmentVariables = { + OLLAMA_KEEP_ALIVE = "1h"; + }; + }; + + # Wyoming Faster Whisper (ASR) + whisper = { + enable = true; # Default: true + model = "tiny-int8"; # tiny-int8, small, medium, large-v3 + language = "en"; # Language code + device = "cuda"; # "cuda" or "cpu" + uri = "tcp://127.0.0.1:10300"; + }; + + # Wyoming Piper (TTS) + piper = { + enable = true; # Default: true + voice = "en_US-ryan-high"; # Voice model + uri = "tcp://127.0.0.1:10200"; + }; + + # Wyoming OpenWakeWord + openwakeword = { + enable = false; # Default: false + preloadModels = [ "ok_nabu" ]; # Wake word models + uri = "tcp://127.0.0.1:10400"; + }; +}; +``` + +### GPU Configuration + +#### NVIDIA GPU + +```nix +{ + services.xserver.videoDrivers = [ "nvidia" ]; + hardware.graphics.enable = true; + hardware.nvidia.modesetting.enable = true; + + services.agent-cli = { + enable = true; + ollama.acceleration = "cuda"; + whisper.device = "cuda"; + }; +} +``` + +#### AMD GPU (ROCm) + +```nix +{ + hardware.graphics.extraPackages = with pkgs; [ + rocmPackages.clr.icd + ]; + + services.agent-cli = { + enable = true; + ollama.acceleration = "rocm"; + whisper.device = "cpu"; # Whisper doesn't support ROCm + }; +} +``` + +## Services Overview + +| Service | Port | Purpose | Systemd Service | +|---------|------|---------|-----------------| +| Ollama | 11434 | Local LLM | `ollama.service` | +| Whisper | 10300 | Speech-to-text | `wyoming-faster-whisper-agent-cli.service` | +| Piper | 10200 | Text-to-speech | `wyoming-piper-agent-cli.service` | +| OpenWakeWord | 10400 | Wake word | `wyoming-openwakeword.service` | + +## Configuration + +Create `~/.config/agent-cli/config.toml`: + +```toml +[llm] +provider = "ollama" +model = "qwen3:4b" + +[asr] +provider = "wyoming" + +[tts] +provider = "wyoming" + +[services] +ollama_url = "http://localhost:11434" +wyoming_asr_uri = "tcp://localhost:10300" +wyoming_tts_uri = "tcp://localhost:10200" +``` + +## Troubleshooting + +### Check Services + +```bash +systemctl status ollama +systemctl status wyoming-faster-whisper-agent-cli +systemctl status wyoming-piper-agent-cli +journalctl -u ollama -f +``` + +### GPU Issues + +```bash +nvidia-smi # Check NVIDIA +rocminfo # Check AMD ROCm +``` + +## Using with Direnv + +Create `.envrc`: +```bash +use flake +``` + +Then: +```bash +direnv allow +``` + +## Updating + +```bash +nix flake update +sudo nixos-rebuild switch --flake .#yourhostname +``` diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..774c89940 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1754214453, + "narHash": "sha256-Q/I2xJn/j1wpkGhWkQnm20nShYnG7TI99foDBpXm1SY=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5b09dc45f24cf32316283e62aec81ffee3c3e376", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..3737dd1b3 --- /dev/null +++ b/flake.nix @@ -0,0 +1,215 @@ +{ + description = "agent-cli: Local-first AI-powered command-line tools"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + }; + + outputs = + { self, nixpkgs }: + let + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + in + { + # Development shell with system dependencies + # Python packages are managed by uv (much simpler than fighting nixpkgs) + devShells = forAllSystems ( + system: + let + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; + in + { + default = pkgs.mkShell { + packages = with pkgs; [ + # Python via uv (handles all Python deps) + uv + python312 + + # System dependencies for PyAudio/sounddevice + portaudio + pkg-config + + # Audio tools + ffmpeg + sox + + # Build tools + gcc + git + ]; + + shellHook = '' + echo "agent-cli development environment" + echo "" + echo "Setup: uv sync --all-extras" + echo "Run: uv run agent-cli --help" + echo "" + + export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [ pkgs.portaudio ]}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + ''; + }; + } + ); + + # NixOS module for configuring AI services (ollama, wyoming-*) + # Install agent-cli itself via: uv tool install agent-cli + nixosModules.default = + { + config, + lib, + pkgs, + ... + }: + let + cfg = config.services.agent-cli; + in + { + options.services.agent-cli = { + enable = lib.mkEnableOption "agent-cli AI backend services"; + + # Ollama LLM + ollama = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable Ollama LLM service"; + }; + acceleration = lib.mkOption { + type = lib.types.enum [ + "cuda" + "rocm" + "cpu" + ]; + default = "cpu"; + description = "Hardware acceleration for Ollama"; + }; + host = lib.mkOption { + type = lib.types.str; + default = "127.0.0.1"; + description = "Host address for Ollama"; + }; + environmentVariables = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { + OLLAMA_KEEP_ALIVE = "1h"; + }; + description = "Environment variables for Ollama"; + }; + }; + + # Wyoming Faster Whisper (ASR) + whisper = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable Wyoming Faster Whisper ASR"; + }; + model = lib.mkOption { + type = lib.types.str; + default = "tiny-int8"; + description = "Whisper model (tiny-int8, small, medium, large-v3)"; + }; + language = lib.mkOption { + type = lib.types.str; + default = "en"; + description = "Language for transcription"; + }; + device = lib.mkOption { + type = lib.types.enum [ + "cuda" + "cpu" + ]; + default = "cpu"; + description = "Device for Whisper"; + }; + uri = lib.mkOption { + type = lib.types.str; + default = "tcp://127.0.0.1:10300"; + description = "URI for Whisper server"; + }; + }; + + # Wyoming Piper (TTS) + piper = { + enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Enable Wyoming Piper TTS"; + }; + voice = lib.mkOption { + type = lib.types.str; + default = "en_US-ryan-high"; + description = "Piper voice"; + }; + uri = lib.mkOption { + type = lib.types.str; + default = "tcp://127.0.0.1:10200"; + description = "URI for Piper server"; + }; + }; + + # Wyoming OpenWakeWord + openwakeword = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Enable Wyoming OpenWakeWord"; + }; + preloadModels = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ "ok_nabu" ]; + description = "Wake word models to preload"; + }; + uri = lib.mkOption { + type = lib.types.str; + default = "tcp://127.0.0.1:10400"; + description = "URI for OpenWakeWord server"; + }; + }; + }; + + config = lib.mkIf cfg.enable { + # Ollama + services.ollama = lib.mkIf cfg.ollama.enable { + enable = true; + acceleration = cfg.ollama.acceleration; + host = cfg.ollama.host; + environmentVariables = cfg.ollama.environmentVariables; + }; + + # Wyoming Faster Whisper + services.wyoming.faster-whisper.servers.agent-cli = lib.mkIf cfg.whisper.enable { + enable = true; + model = cfg.whisper.model; + language = cfg.whisper.language; + device = cfg.whisper.device; + uri = cfg.whisper.uri; + }; + + # Wyoming Piper + services.wyoming.piper.servers.agent-cli = lib.mkIf cfg.piper.enable { + enable = true; + voice = cfg.piper.voice; + uri = cfg.piper.uri; + }; + + # Wyoming OpenWakeWord + services.wyoming.openwakeword = lib.mkIf cfg.openwakeword.enable { + enable = true; + preloadModels = cfg.openwakeword.preloadModels; + uri = cfg.openwakeword.uri; + }; + }; + }; + }; +} diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 57ca9c0a6..000000000 --- a/shell.nix +++ /dev/null @@ -1,16 +0,0 @@ -# nix-direnv file -{ pkgs ? import {}}: - -pkgs.mkShell { - packages = [ - pkgs.portaudio - pkgs.ffmpeg - pkgs.pkg-config - pkgs.gcc - pkgs.python3 - ]; - - shellHook = '' - export LD_LIBRARY_PATH=${pkgs.lib.makeLibraryPath [ pkgs.portaudio ]}:$LD_LIBRARY_PATH - ''; -} From f2e7738fe621b710ff66fcb752a53b3440a3cbab Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 16 Jan 2026 07:30:00 -0800 Subject: [PATCH 2/5] feat(nix): auto-sync Python deps on shell entry - shellHook now runs `uv sync --all-extras` automatically - No manual setup needed - just `nix develop` and use agent-cli - Update docs to reflect simpler workflow --- docs/installation/flake.md | 15 +++++++-------- flake.nix | 13 +++++++------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/installation/flake.md b/docs/installation/flake.md index 3a5f5d318..e6e57d19c 100644 --- a/docs/installation/flake.md +++ b/docs/installation/flake.md @@ -23,17 +23,16 @@ git clone https://github.com/basnijholt/agent-cli.git cd agent-cli nix develop -# Inside the shell: -uv sync --all-extras -uv run agent-cli --help +# Python deps are auto-installed, ready to use: +agent-cli --help ``` -The dev shell provides: -- Python 3.12 with uv for package management -- System dependencies (portaudio, ffmpeg, sox) -- Build tools (gcc, pkg-config) +The dev shell automatically: +- Syncs Python dependencies via uv +- Sets up system dependencies (portaudio, ffmpeg, sox) +- Configures LD_LIBRARY_PATH for audio libraries -### Install agent-cli +### Install agent-cli System-Wide ```bash # Using uv (recommended) diff --git a/flake.nix b/flake.nix index 3737dd1b3..b15c3e29d 100644 --- a/flake.nix +++ b/flake.nix @@ -49,13 +49,14 @@ ]; shellHook = '' - echo "agent-cli development environment" - echo "" - echo "Setup: uv sync --all-extras" - echo "Run: uv run agent-cli --help" - echo "" - export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [ pkgs.portaudio ]}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + + # Auto-sync Python dependencies + if [ -f pyproject.toml ]; then + echo "Syncing Python dependencies..." + uv sync --all-extras --quiet + echo "Ready! Run: agent-cli --help" + fi ''; }; } From 24f7767c90dee6ceec6a84101e3f8154420130ac Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 16 Jan 2026 22:38:06 +0100 Subject: [PATCH 3/5] style: format flake.nix with nixfmt --- flake.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index b15c3e29d..61f7a1dda 100644 --- a/flake.nix +++ b/flake.nix @@ -49,7 +49,9 @@ ]; shellHook = '' - export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [ pkgs.portaudio ]}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + export LD_LIBRARY_PATH="${ + pkgs.lib.makeLibraryPath [ pkgs.portaudio ] + }''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" # Auto-sync Python dependencies if [ -f pyproject.toml ]; then From 48506248c6c1e6545cc1299248bd6ab51b1d2f67 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 16 Jan 2026 22:38:41 +0100 Subject: [PATCH 4/5] fix: remove unnecessary pyproject.toml check in shellHook --- flake.nix | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/flake.nix b/flake.nix index 61f7a1dda..1145fb68d 100644 --- a/flake.nix +++ b/flake.nix @@ -52,13 +52,8 @@ export LD_LIBRARY_PATH="${ pkgs.lib.makeLibraryPath [ pkgs.portaudio ] }''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" - - # Auto-sync Python dependencies - if [ -f pyproject.toml ]; then - echo "Syncing Python dependencies..." - uv sync --all-extras --quiet - echo "Ready! Run: agent-cli --help" - fi + uv sync --all-extras --quiet + echo "Ready! Run: agent-cli --help" ''; }; } From 3c46e850f803b7a6aedf0882de1ba233b1b01eb9 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Fri, 16 Jan 2026 22:39:23 +0100 Subject: [PATCH 5/5] chore: increase OLLAMA_KEEP_ALIVE default to 2h --- flake.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index 1145fb68d..69fdb7667 100644 --- a/flake.nix +++ b/flake.nix @@ -99,7 +99,7 @@ environmentVariables = lib.mkOption { type = lib.types.attrsOf lib.types.str; default = { - OLLAMA_KEEP_ALIVE = "1h"; + OLLAMA_KEEP_ALIVE = "2h"; }; description = "Environment variables for Ollama"; };