diff --git a/modules/misc/news/2026/06/2026-06-02_12-00-00.nix b/modules/misc/news/2026/06/2026-06-02_12-00-00.nix new file mode 100644 index 000000000..a69baf618 --- /dev/null +++ b/modules/misc/news/2026/06/2026-06-02_12-00-00.nix @@ -0,0 +1,15 @@ +{ config, ... }: +{ + time = "2026-06-02T12:00:00+00:00"; + condition = config.programs.pi-coding-agent.enable; + message = '' + A new module is available: `programs.pi-coding-agent`. + + The module supports declarative configuration of Pi Coding Agent + including settings, keybindings, models, and global context + (AGENTS.md). + + For MCP server support, use the existing `programs.mcp` module + which writes to `~/.config/mcp/mcp.json`. + ''; +} diff --git a/modules/programs/pi-coding-agent.nix b/modules/programs/pi-coding-agent.nix new file mode 100644 index 000000000..324d6e9c5 --- /dev/null +++ b/modules/programs/pi-coding-agent.nix @@ -0,0 +1,223 @@ +{ + config, + lib, + pkgs, + ... +}: +let + inherit (lib) + literalExpression + mkEnableOption + mkIf + mkOption + mkPackageOption + ; + + cfg = config.programs.pi-coding-agent; + + jsonFormat = pkgs.formats.json { }; + + isPathLike = + content: + lib.isPath content + || (builtins.isString content && lib.hasPrefix "${builtins.storeDir}/" content) + || lib.isDerivation content; + + upstreamConfigDir = "${config.home.homeDirectory}/.pi/agent"; + + packageWithExtraPackages = + if cfg.package != null && cfg.extraPackages != [ ] then + pkgs.symlinkJoin { + inherit (cfg.package) meta; + name = "${lib.getName cfg.package}-wrapped-${lib.getVersion cfg.package}"; + paths = [ cfg.package ]; + preferLocalBuild = true; + nativeBuildInputs = [ pkgs.makeWrapper ]; + postBuild = '' + wrapProgram $out/bin/pi \ + --suffix PATH : ${lib.makeBinPath cfg.extraPackages} + ''; + } + else + cfg.package; +in +{ + meta.maintainers = with lib.hm.maintainers; [ semi710 ]; + + options.programs.pi-coding-agent = { + enable = mkEnableOption "pi-coding-agent"; + + package = mkPackageOption pkgs "pi-coding-agent" { nullable = true; }; + + extraPackages = mkOption { + type = with lib.types; listOf package; + default = [ ]; + example = literalExpression "[ pkgs.nodejs pkgs.bun ]"; + description = '' + Extra packages available to Pi Coding Agent. + These are added to the PATH of the wrapped pi binary. + + Needed for packages installed by pi (e.g. + {command}`npm:@termdraw/pi` requires {command}`npm` and + {command}`bun`). + ''; + }; + + configDir = mkOption { + type = lib.types.str; + default = upstreamConfigDir; + defaultText = literalExpression ''"''${config.home.homeDirectory}/.pi/agent"''; + example = literalExpression ''"''${config.xdg.configHome}/pi/agent"''; + description = '' + Directory holding Pi Coding Agent's configuration files. + + Defaults to {file}`~/.pi/agent`, matching the upstream + {command}`pi` CLI default. The {env}`PI_CODING_AGENT_DIR` + environment variable is exported automatically whenever the + directory differs from this default so the CLI reads + configuration from the same location. + ''; + }; + + settings = mkOption { + inherit (jsonFormat) type; + default = { }; + example = { + defaultProvider = "anthropic"; + defaultModel = "claude-sonnet-4-20250514"; + defaultThinkingLevel = "medium"; + theme = "dark"; + packages = [ + "npm:@termdraw/pi" + "npm:pi-mcp-adapter" + ]; + compaction = { + enabled = true; + reserveTokens = 16384; + keepRecentTokens = 20000; + }; + retry = { + enabled = true; + maxRetries = 3; + }; + enabledModels = [ + "claude-*" + "gpt-4o" + ]; + }; + description = '' + Configuration written to + {file}`~/.pi/agent/settings.json`. + See for the + documentation. + ''; + }; + + keybindings = mkOption { + inherit (jsonFormat) type; + default = { }; + example = { + "tui.editor.cursorUp" = [ + "up" + "ctrl+p" + ]; + "tui.editor.cursorDown" = [ + "down" + "ctrl+n" + ]; + "tui.editor.deleteWordBackward" = [ + "ctrl+w" + "alt+backspace" + ]; + }; + description = '' + Keybindings configuration written to + {file}`~/.pi/agent/keybindings.json`. + See for the + documentation. + ''; + }; + + models = mkOption { + inherit (jsonFormat) type; + default = { }; + example = { + providers = { + ollama = { + baseUrl = "http://localhost:11434/v1"; + api = "openai-completions"; + apiKey = "ollama"; + models = [ { id = "llama3.1:8b"; } ]; + }; + }; + }; + description = '' + Custom model providers written to + {file}`~/.pi/agent/models.json`. + + Each provider entry may contain `baseUrl`, + `api`, `apiKey`, `compat`, and a `models` + list with `id`, `name`, `reasoning`, etc. + + See for the + documentation. + ''; + }; + + context = mkOption { + type = lib.types.either lib.types.lines lib.types.path; + default = ""; + description = '' + Global context for Pi Coding Agent. + + The value is either: + - Inline content as a string + - A path to a file containing the content + + The configured content is written to + {file}`AGENTS.md` inside + {option}`programs.pi-coding-agent.configDir` + (default {file}`~/.pi/agent/AGENTS.md`). + ''; + example = literalExpression "./pi-context.md"; + }; + }; + + config = mkIf cfg.enable { + home = { + packages = mkIf (packageWithExtraPackages != null) [ + packageWithExtraPackages + ]; + + sessionVariables = lib.mkIf (cfg.configDir != upstreamConfigDir) { + PI_CODING_AGENT_DIR = cfg.configDir; + }; + + file = lib.mkMerge [ + (mkIf (cfg.settings != { }) { + "${cfg.configDir}/settings.json".source = + jsonFormat.generate "pi-coding-agent-settings.json" cfg.settings; + }) + + (mkIf (cfg.keybindings != { }) { + "${cfg.configDir}/keybindings.json".source = + jsonFormat.generate "pi-coding-agent-keybindings.json" cfg.keybindings; + }) + + (mkIf (cfg.models != { }) { + "${cfg.configDir}/models.json".source = + jsonFormat.generate "pi-coding-agent-models.json" cfg.models; + }) + + ( + if isPathLike cfg.context then + { "${cfg.configDir}/AGENTS.md".source = cfg.context; } + else + (mkIf (cfg.context != "") { + "${cfg.configDir}/AGENTS.md".text = cfg.context; + }) + ) + ]; + }; + }; +} diff --git a/tests/modules/programs/pi-coding-agent/context-empty.nix b/tests/modules/programs/pi-coding-agent/context-empty.nix new file mode 100644 index 000000000..0afe7dae0 --- /dev/null +++ b/tests/modules/programs/pi-coding-agent/context-empty.nix @@ -0,0 +1,9 @@ +{ + programs.pi-coding-agent = { + enable = true; + context = ""; + }; + nmt.script = '' + assertPathNotExists home-files/.pi/agent/AGENTS.md + ''; +} diff --git a/tests/modules/programs/pi-coding-agent/context-inline.md b/tests/modules/programs/pi-coding-agent/context-inline.md new file mode 100644 index 000000000..1ee37f5bd --- /dev/null +++ b/tests/modules/programs/pi-coding-agent/context-inline.md @@ -0,0 +1,4 @@ +# Global Pi Context + +Always use TypeScript strict mode. +Follow the project's existing code style. diff --git a/tests/modules/programs/pi-coding-agent/context-inline.nix b/tests/modules/programs/pi-coding-agent/context-inline.nix new file mode 100644 index 000000000..681b71d91 --- /dev/null +++ b/tests/modules/programs/pi-coding-agent/context-inline.nix @@ -0,0 +1,16 @@ +{ + programs.pi-coding-agent = { + enable = true; + context = '' + # Global Pi Context + + Always use TypeScript strict mode. + Follow the project's existing code style. + ''; + }; + nmt.script = '' + assertFileExists home-files/.pi/agent/AGENTS.md + assertFileContent home-files/.pi/agent/AGENTS.md \ + ${./context-inline.md} + ''; +} diff --git a/tests/modules/programs/pi-coding-agent/context-path.nix b/tests/modules/programs/pi-coding-agent/context-path.nix new file mode 100644 index 000000000..cd6ca6db1 --- /dev/null +++ b/tests/modules/programs/pi-coding-agent/context-path.nix @@ -0,0 +1,11 @@ +{ + programs.pi-coding-agent = { + enable = true; + context = ./context-inline.md; + }; + nmt.script = '' + assertFileExists home-files/.pi/agent/AGENTS.md + assertFileContent home-files/.pi/agent/AGENTS.md \ + ${./context-inline.md} + ''; +} diff --git a/tests/modules/programs/pi-coding-agent/custom-config-dir.nix b/tests/modules/programs/pi-coding-agent/custom-config-dir.nix new file mode 100644 index 000000000..32b353414 --- /dev/null +++ b/tests/modules/programs/pi-coding-agent/custom-config-dir.nix @@ -0,0 +1,11 @@ +{ + programs.pi-coding-agent = { + enable = true; + configDir = "/home/testuser/.config/pi/agent"; + }; + nmt.script = '' + # Verify env var is set for non-default configDir + assertFileRegex home-path/etc/profile.d/hm-session-vars.sh \ + 'PI_CODING_AGENT_DIR' + ''; +} diff --git a/tests/modules/programs/pi-coding-agent/default.nix b/tests/modules/programs/pi-coding-agent/default.nix new file mode 100644 index 000000000..2069a8a6e --- /dev/null +++ b/tests/modules/programs/pi-coding-agent/default.nix @@ -0,0 +1,10 @@ +{ + pi-coding-agent-settings = ./settings.nix; + pi-coding-agent-empty-settings = ./empty-settings.nix; + pi-coding-agent-keybindings = ./keybindings.nix; + pi-coding-agent-context-inline = ./context-inline.nix; + pi-coding-agent-context-path = ./context-path.nix; + pi-coding-agent-context-empty = ./context-empty.nix; + pi-coding-agent-custom-config-dir = ./custom-config-dir.nix; + pi-coding-agent-models = ./models.nix; +} diff --git a/tests/modules/programs/pi-coding-agent/empty-settings.nix b/tests/modules/programs/pi-coding-agent/empty-settings.nix new file mode 100644 index 000000000..fe4e1a8cd --- /dev/null +++ b/tests/modules/programs/pi-coding-agent/empty-settings.nix @@ -0,0 +1,9 @@ +{ + programs.pi-coding-agent = { + enable = true; + settings = { }; + }; + nmt.script = '' + assertPathNotExists home-files/.pi/agent/settings.json + ''; +} diff --git a/tests/modules/programs/pi-coding-agent/keybindings.json b/tests/modules/programs/pi-coding-agent/keybindings.json new file mode 100644 index 000000000..0d12681d6 --- /dev/null +++ b/tests/modules/programs/pi-coding-agent/keybindings.json @@ -0,0 +1,14 @@ +{ + "tui.editor.cursorDown": [ + "down", + "ctrl+n" + ], + "tui.editor.cursorUp": [ + "up", + "ctrl+p" + ], + "tui.editor.deleteWordBackward": [ + "ctrl+w", + "alt+backspace" + ] +} diff --git a/tests/modules/programs/pi-coding-agent/keybindings.nix b/tests/modules/programs/pi-coding-agent/keybindings.nix new file mode 100644 index 000000000..59e0db44b --- /dev/null +++ b/tests/modules/programs/pi-coding-agent/keybindings.nix @@ -0,0 +1,24 @@ +{ + programs.pi-coding-agent = { + enable = true; + keybindings = { + "tui.editor.cursorUp" = [ + "up" + "ctrl+p" + ]; + "tui.editor.cursorDown" = [ + "down" + "ctrl+n" + ]; + "tui.editor.deleteWordBackward" = [ + "ctrl+w" + "alt+backspace" + ]; + }; + }; + nmt.script = '' + assertFileExists home-files/.pi/agent/keybindings.json + assertFileContent home-files/.pi/agent/keybindings.json \ + ${./keybindings.json} + ''; +} diff --git a/tests/modules/programs/pi-coding-agent/models.json b/tests/modules/programs/pi-coding-agent/models.json new file mode 100644 index 000000000..9ae677370 --- /dev/null +++ b/tests/modules/programs/pi-coding-agent/models.json @@ -0,0 +1,14 @@ +{ + "providers": { + "litellm": { + "api": "openai-completions", + "apiKey": "ollama", + "baseUrl": "http://localhost:11434/v1", + "models": [ + { + "id": "llama3.1:8b" + } + ] + } + } +} diff --git a/tests/modules/programs/pi-coding-agent/models.nix b/tests/modules/programs/pi-coding-agent/models.nix new file mode 100644 index 000000000..d850ea52c --- /dev/null +++ b/tests/modules/programs/pi-coding-agent/models.nix @@ -0,0 +1,20 @@ +{ + programs.pi-coding-agent = { + enable = true; + models = { + providers = { + litellm = { + baseUrl = "http://localhost:11434/v1"; + api = "openai-completions"; + apiKey = "ollama"; + models = [ { id = "llama3.1:8b"; } ]; + }; + }; + }; + }; + nmt.script = '' + assertFileExists home-files/.pi/agent/models.json + assertFileContent home-files/.pi/agent/models.json \ + ${./models.json} + ''; +} diff --git a/tests/modules/programs/pi-coding-agent/settings.json b/tests/modules/programs/pi-coding-agent/settings.json new file mode 100644 index 000000000..c7c3e220b --- /dev/null +++ b/tests/modules/programs/pi-coding-agent/settings.json @@ -0,0 +1,19 @@ +{ + "compaction": { + "enabled": true, + "keepRecentTokens": 20000, + "reserveTokens": 16384 + }, + "defaultModel": "claude-sonnet-4-20250514", + "defaultProvider": "anthropic", + "defaultThinkingLevel": "medium", + "enabledModels": [ + "claude-*", + "gpt-4o" + ], + "retry": { + "enabled": true, + "maxRetries": 3 + }, + "theme": "dark" +} diff --git a/tests/modules/programs/pi-coding-agent/settings.nix b/tests/modules/programs/pi-coding-agent/settings.nix new file mode 100644 index 000000000..e4d2a6087 --- /dev/null +++ b/tests/modules/programs/pi-coding-agent/settings.nix @@ -0,0 +1,29 @@ +{ + programs.pi-coding-agent = { + enable = true; + settings = { + defaultProvider = "anthropic"; + defaultModel = "claude-sonnet-4-20250514"; + defaultThinkingLevel = "medium"; + theme = "dark"; + compaction = { + enabled = true; + reserveTokens = 16384; + keepRecentTokens = 20000; + }; + retry = { + enabled = true; + maxRetries = 3; + }; + enabledModels = [ + "claude-*" + "gpt-4o" + ]; + }; + }; + nmt.script = '' + assertFileExists home-files/.pi/agent/settings.json + assertFileContent home-files/.pi/agent/settings.json \ + ${./settings.json} + ''; +}