{ config, lib, pkgs, ... }: with lib; let cfg = config.coding.agents.pi; mcpCfg = config.programs.mcp or null; in { options.coding.agents.pi = { enable = mkEnableOption "Pi agent management via canonical agent.toml definitions"; path = mkOption { type = types.str; default = ".pi/agent"; description = '' Relative path (inside the Home Manager user's home) where Pi agent config should be materialized. Defaults to `.pi/agent`, i.e. `~/.pi/agent`. ''; example = ".config/pi/agent"; }; mcpServers = mkOption { type = types.attrsOf types.anything; default = if mcpCfg != null then mcpCfg.servers else {}; defaultText = literalExpression "config.programs.mcp.servers"; description = '' MCP server configurations for Pi (pi-mcp-adapter). Written to `${cfg.path}/mcp.json`. Automatically inherits from config.programs.mcp.servers. ''; }; agentsInput = mkOption { type = types.nullOr types.anything; default = null; description = '' The `agents` flake input (your personal AGENTS repo). When set, the primary agent's system prompt is rendered as SYSTEM.md, all agents are listed in AGENTS.md, and subagent .md files are deployed. ''; }; modelOverrides = mkOption { type = types.attrsOf types.str; default = {}; description = '' Per-agent model overrides for Pi subagents. Maps agent slug to model string, e.g.: { chiron = "anthropic/claude-sonnet-4"; chiron-forge = "anthropic/claude-sonnet-4"; } ''; }; externalSkills = mkOption { type = types.listOf (types.submodule { options = { src = mkOption { type = types.anything; description = "Flake input pointing to a skills repository root."; }; skillsDir = mkOption { type = types.str; default = "skills"; description = '' Subdirectory inside src that contains skill folders. ''; }; selectSkills = mkOption { type = types.nullOr (types.listOf types.str); default = null; description = '' List of skill names to cherry-pick from this source. null means include every skill found in skillsDir. ''; }; }; }); default = []; description = '' External skill sources passed to mkOpencodeSkills. Each entry maps directly to an element of the externalSkills list accepted by the AGENTS flake's lib.mkOpencodeSkills. ''; example = literalExpression '' [ { src = inputs.skills-anthropic; selectSkills = [ "claude-api" ]; } { src = inputs.skills-vercel; } ] ''; }; primaryAgent = mkOption { type = types.nullOr types.str; default = null; description = '' Override which canonical agent is used as primary for SYSTEM.md. When null, the first agent with mode="primary" is used. ''; }; settings = mkOption { type = types.submodule { freeformType = types.attrsOf types.anything; options = { packages = mkOption { type = types.listOf types.str; default = []; description = '' Pi packages to install (npm:, git:, or local paths). These are written to `${cfg.path}/settings.json`. ''; }; defaultProvider = mkOption { type = types.nullOr types.str; default = null; description = "Default LLM provider (e.g. 'anthropic', 'openai', 'zai')."; }; defaultModel = mkOption { type = types.nullOr types.str; default = null; description = "Default model ID."; }; defaultThinkingLevel = mkOption { type = types.nullOr (types.enum ["off" "minimal" "low" "medium" "high" "xhigh"]); default = null; description = "Default extended thinking level."; }; theme = mkOption { type = types.nullOr types.str; default = null; description = "Pi theme name."; }; hideThinkingBlock = mkOption { type = types.nullOr types.bool; default = null; description = "Hide thinking blocks in output."; }; quietStartup = mkOption { type = types.nullOr types.bool; default = null; description = "Hide startup header."; }; compaction = mkOption { type = types.nullOr (types.submodule { options = { enabled = mkOption { type = types.nullOr types.bool; default = null; }; reserveTokens = mkOption { type = types.nullOr types.int; default = null; }; keepRecentTokens = mkOption { type = types.nullOr types.int; default = null; }; }; }); default = null; description = "Auto-compaction settings."; }; enabledModels = mkOption { type = types.nullOr (types.listOf types.str); default = null; description = "Model patterns for Ctrl+P cycling."; }; sessionDir = mkOption { type = types.nullOr types.str; default = null; description = "Directory where session files are stored."; }; extensions = mkOption { type = types.listOf types.str; default = []; description = "Local extension file paths or directories."; }; skills = mkOption { type = types.listOf types.str; default = []; description = "Local skill file paths or directories."; }; }; }; default = {}; description = '' Pi settings written to `${cfg.path}/settings.json`. Only non-null values are included in the generated JSON. See pi docs/settings.md for all options. ''; }; }; config = mkIf cfg.enable (let basePath = lib.removeSuffix "/" cfg.path; # Build settings.json by filtering out null values recursively filterNulls = attrs: lib.filterAttrs (_: v: v != null) ( builtins.mapAttrs (_: v: if builtins.isAttrs v then let filtered = filterNulls v; in if filtered == {} then null else filtered else v) attrs ); piSettings = filterNulls cfg.settings; # Rendered agents (only computed when agentsInput is set) rendered = if cfg.agentsInput != null then (import ../../../../lib {inherit lib;}).agents.renderForPi { inherit pkgs; canonical = cfg.agentsInput.lib.loadAgents; modelOverrides = cfg.modelOverrides; primaryAgent = cfg.primaryAgent; } else null; # Dynamic home.file entries for agent .md files agentFiles = if cfg.agentsInput != null then let agentNames = builtins.attrNames cfg.agentsInput.lib.loadAgents; in builtins.listToAttrs ( map (name: { name = "${basePath}/agents/${name}.md"; value = {source = "${rendered}/agents/${name}.md";}; }) agentNames ) else {}; in { home.file = mkMerge [ # ── MCP servers from programs.mcp → ${cfg.path}/mcp.json ─────── (mkIf (cfg.mcpServers != {}) { "${basePath}/mcp.json".text = builtins.toJSON {mcpServers = cfg.mcpServers;}; }) # ── ${cfg.path}/settings.json ────────────────────────────────── { "${basePath}/settings.json".text = builtins.toJSON piSettings; } # ── AGENTS.md — agent descriptions and specialist listing ────── (mkIf (cfg.agentsInput != null) { "${basePath}/AGENTS.md".source = "${rendered}/AGENTS.md"; }) # ── SYSTEM.md — primary agent's system prompt ────────────────── (mkIf (cfg.agentsInput != null) { "${basePath}/SYSTEM.md".source = "${rendered}/SYSTEM.md"; }) # ── Agents — pi-subagents .md files ──────────────────────────── agentFiles # ── Skills symlinked from AGENTS repo ────────────────────────── (mkIf (cfg.agentsInput != null) { "${basePath}/skills".source = cfg.agentsInput.lib.mkOpencodeSkills { inherit pkgs; customSkills = "${cfg.agentsInput}/skills"; externalSkills = map ( entry: {inherit (entry) src skillsDir;} // optionalAttrs (entry.selectSkills != null) {inherit (entry) selectSkills;} ) cfg.externalSkills; }; }) ]; }); }