diff --git a/docs/packages/notesmd-cli.md b/docs/packages/notesmd-cli.md deleted file mode 100644 index 5afd135..0000000 --- a/docs/packages/notesmd-cli.md +++ /dev/null @@ -1,117 +0,0 @@ -# notesmd-cli - -Obsidian CLI (Community) - Interact with Obsidian in the terminal. - -## Description - -notesmd-cli is a command-line interface for interacting with Obsidian, the popular knowledge management and note-taking application. It allows you to create, search, and manipulate notes directly from the terminal. - -## Features - -- 📝 **Note Creation**: Create new notes from the command line -- 🔍 **Search**: Search through your Obsidian vault -- 📂 **Vault Management**: Interact with your vault structure -- 🔗 **WikiLink Support**: Work with Obsidian's WikiLink format -- 🏷️ **Tag Support**: Manage and search by tags -- ⚡ **Fast**: Lightweight Go binary with no external dependencies - -## Installation - -### Via Overlay - -```nix -{pkgs, ...}: { - environment.systemPackages = with pkgs; [ - notesmd-cli - ]; -} -``` - -### Direct Reference - -```nix -{pkgs, ...}: { - environment.systemPackages = with pkgs; [ - inputs.m3ta-nixpkgs.packages.${pkgs.system}.notesmd-cli - ]; -} -``` - -### Run Directly - -```bash -nix run git+https://code.m3ta.dev/m3tam3re/nixpkgs#notesmd-cli -``` - -## Usage - -### Basic Commands - -```bash -# Show help -notesmd-cli --help - -# Create a new note -notesmd-cli new "My Note Title" - -# Search notes -notesmd-cli search "search term" - -# List notes -notesmd-cli list -``` - -### Working with Vaults - -```bash -# Specify vault path -notesmd-cli --vault /path/to/vault new "Note Title" - -# Open a note in Obsidian -notesmd-cli open "Note Name" -``` - -### Advanced Usage - -```bash -# Search with tags -notesmd-cli search --tag "project" - -# Append to existing note -notesmd-cli append "Note Name" "Additional content" -``` - -## Configuration - -### Environment Variables - -- `OBSIDIAN_VAULT`: Default vault path - -### Command Line Options - -Run `notesmd-cli --help` for a complete list of options. - -## Build Information - -- **Version**: 0.3.0 -- **Language**: Go -- **License**: MIT -- **Source**: [GitHub](https://github.com/Yakitrak/notesmd-cli) -- **Vendor Hash**: null (no external dependencies) - -## Platform Support - -- Linux -- macOS (Unix systems) - -## Notes - -- No vendor dependencies (pure Go stdlib) -- The binary is named `notesmd-cli` (not `notesmd`) -- This is the community CLI, not the official Obsidian CLI - -## Related - -- [Obsidian](https://obsidian.md) - The Obsidian application -- [Adding Packages](../guides/adding-packages.md) - How to add new packages -- [Quick Start](../QUICKSTART.md) - Getting started guide diff --git a/flake.nix b/flake.nix index 4b77cd1..7af484e 100644 --- a/flake.nix +++ b/flake.nix @@ -77,10 +77,7 @@ }; # Library functions - helper utilities for your configuration - lib = forAllSystems (system: let - pkgs = pkgsFor system; - in - import ./lib {lib = pkgs.lib;}); + lib = forAllSystems (system: import ./lib {lib = nixpkgs.lib;}); # Development shells for various programming environments # Usage: nix develop .# @@ -104,6 +101,9 @@ ${pkgs.alejandra}/bin/alejandra --check ${./.} touch $out ''; + # Lib unit tests + lib-agents = import ./tests/lib/agents-test.nix; + lib-coding-rules = import ./tests/lib/coding-rules-test.nix; }); # Templates for creating new packages/modules diff --git a/lib/coding-rules.nix b/lib/coding-rules.nix index 2f3812a..8b4abed 100644 --- a/lib/coding-rules.nix +++ b/lib/coding-rules.nix @@ -1,4 +1,4 @@ -# Opencode rules management utilities +# Coding rules management utilities # # This module provides functions to configure Opencode agent rules across # multiple projects. Rules are defined in the AGENTS repository and can be diff --git a/modules/home-manager/coding/agents/claude-code.nix b/modules/home-manager/coding/agents/claude-code.nix index 7a5d402..2309125 100644 --- a/modules/home-manager/coding/agents/claude-code.nix +++ b/modules/home-manager/coding/agents/claude-code.nix @@ -3,144 +3,87 @@ lib, pkgs, ... -}: -with lib; let - cfg = config.coding.agents.claude-code; - mcpCfg = config.programs.mcp or null; -in { - options.coding.agents.claude-code = { - enable = mkEnableOption "Claude Code agent management via canonical agent.toml definitions"; +}: let + shared = import ./shared-options.nix {inherit lib;}; +in + with lib; let + cfg = config.coding.agents.claude-code; + mcpCfg = config.programs.mcp or null; + in { + options.coding.agents.claude-code = { + enable = mkEnableOption "Claude Code agent management via canonical agent.toml definitions"; - agentsInput = mkOption { - type = types.nullOr types.anything; - default = null; - description = '' + agentsInput = shared.mkAgentsInputOption '' The `agents` flake input (your personal AGENTS repo). When set, agents are rendered from canonical agent.toml files and symlinked to ~/.claude/agents/. ''; - }; - modelOverrides = mkOption { - type = types.attrsOf types.str; - default = {}; - description = '' - Per-agent model overrides. Maps agent slug to model alias or ID. - Example: { chiron = "claude-sonnet-4-20250514"; } - ''; - example = literalExpression '' - { - chiron = "claude-sonnet-4-20250514"; - "chiron-forge" = "claude-sonnet-4-20250514"; - } - ''; - }; + modelOverrides = shared.mkModelOverridesOption; - 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; } - ] - ''; - }; + externalSkills = shared.externalSkillsOption; - 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 Claude Code. - Merged into ~/.claude/settings.json alongside permissions. - Automatically inherits from config.programs.mcp.servers. - ''; - }; - }; - - config = mkIf cfg.enable (let - agentsLib = (import ../../../../lib {inherit lib;}).agents; - - # Rendered agents + permissions (only if agentsInput is set) - rendered = mkIf (cfg.agentsInput != null) ( - agentsLib.renderForClaudeCode { - inherit pkgs; - canonical = cfg.agentsInput.lib.loadAgents; - modelOverrides = cfg.modelOverrides; - } - ); - - # Merge MCP servers into the rendered settings.json. - # The renderer produces { permissions: { allow, deny } }. - # We add mcpServers on top. - settingsJson = - if cfg.agentsInput != null - then let - renderedSettings = builtins.fromJSON (builtins.readFile "${rendered}/.claude/settings.json"); - withMcp = - if cfg.mcpServers != {} - then renderedSettings // {mcpServers = cfg.mcpServers;} - else renderedSettings; - in - pkgs.writeText "claude-settings.json" (builtins.toJSON withMcp) - else if cfg.mcpServers != {} - then pkgs.writeText "claude-settings.json" (builtins.toJSON {mcpServers = cfg.mcpServers;}) - else null; - in { - # Rendered agent files symlinked to ~/.claude/agents/ - home.file.".claude/agents" = mkIf (cfg.agentsInput != null) { - source = "${rendered}/.claude/agents"; - }; - - # Skills (merged from personal AGENTS repo + optional external skills) - home.file.".claude/skills" = mkIf (cfg.agentsInput != null) { - 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; + 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 Claude Code. + Merged into ~/.claude/settings.json alongside permissions. + Automatically inherits from config.programs.mcp.servers. + ''; }; }; - # Rendered settings.json with permissions + MCP servers - home.file.".claude/settings.json" = mkIf (settingsJson != null) { - source = "${settingsJson}"; - }; - }); -} + config = mkIf cfg.enable (let + agentsLib = (import ../../../../lib {inherit lib;}).agents; + + # Rendered agents + permissions (only if agentsInput is set) + rendered = mkIf (cfg.agentsInput != null) ( + agentsLib.renderForClaudeCode { + inherit pkgs; + canonical = cfg.agentsInput.lib.loadAgents; + modelOverrides = cfg.modelOverrides; + } + ); + + # Merge MCP servers into the rendered settings.json. + # The renderer produces { permissions: { allow, deny } }. + # We add mcpServers on top. + settingsJson = + if cfg.agentsInput != null + then let + renderedSettings = builtins.fromJSON (builtins.readFile "${rendered}/.claude/settings.json"); + withMcp = + if cfg.mcpServers != {} + then renderedSettings // {mcpServers = cfg.mcpServers;} + else renderedSettings; + in + pkgs.writeText "claude-settings.json" (builtins.toJSON withMcp) + else if cfg.mcpServers != {} + then pkgs.writeText "claude-settings.json" (builtins.toJSON {mcpServers = cfg.mcpServers;}) + else null; + in { + # Rendered agent files symlinked to ~/.claude/agents/ + home.file.".claude/agents" = mkIf (cfg.agentsInput != null) { + source = "${rendered}/.claude/agents"; + }; + + # Skills (merged from personal AGENTS repo + optional external skills) + home.file.".claude/skills" = mkIf (cfg.agentsInput != null) { + source = cfg.agentsInput.lib.mkOpencodeSkills { + inherit pkgs; + customSkills = "${cfg.agentsInput}/skills"; + externalSkills = shared.mapExternalSkills cfg.externalSkills; + }; + }; + + # Rendered settings.json with permissions + MCP servers + home.file.".claude/settings.json" = mkIf (settingsJson != null) { + source = "${settingsJson}"; + }; + }); + } diff --git a/modules/home-manager/coding/agents/opencode.nix b/modules/home-manager/coding/agents/opencode.nix index babde2e..ef99aa6 100644 --- a/modules/home-manager/coding/agents/opencode.nix +++ b/modules/home-manager/coding/agents/opencode.nix @@ -3,111 +3,67 @@ lib, pkgs, ... -}: -with lib; let - cfg = config.coding.agents.opencode; -in { - options.coding.agents.opencode = { - enable = mkEnableOption "OpenCode agent management via canonical agent.toml definitions"; +}: let + shared = import ./shared-options.nix {inherit lib;}; +in + with lib; { + options.coding.agents.opencode = { + enable = mkEnableOption "OpenCode agent management via canonical agent.toml definitions"; - agentsInput = mkOption { - type = types.nullOr types.anything; - default = null; - description = '' + agentsInput = shared.mkAgentsInputOption '' The `agents` flake input (your personal AGENTS repo). When set, agents are rendered from canonical agent.toml files and symlinked to ~/.config/opencode/agents/. ''; + + modelOverrides = shared.mkModelOverridesOption; + + externalSkills = shared.externalSkillsOption; }; - modelOverrides = mkOption { - type = types.attrsOf types.str; - default = {}; - description = '' - Per-agent model overrides. Maps agent slug to model string. - Example: { chiron = "anthropic/claude-sonnet-4"; } - ''; - example = literalExpression '' - { - 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. - ''; + config = mkIf config.coding.agents.opencode.enable { + # Rendered agent files symlinked to ~/.config/opencode/agents/ + xdg.configFile."opencode/agents" = let + cfg = config.coding.agents.opencode; + in + mkIf (cfg.agentsInput != null) { + source = (import ../../../../lib {inherit lib;}).agents.renderForOpencode { + inherit pkgs; + canonical = cfg.agentsInput.lib.loadAgents; + modelOverrides = cfg.modelOverrides; }; }; - }); - 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; } - ] - ''; - }; - }; - config = mkIf cfg.enable { - # Rendered agent files symlinked to ~/.config/opencode/agents/ - xdg.configFile."opencode/agents" = mkIf (cfg.agentsInput != null) { - source = (import ../../../../lib {inherit lib;}).agents.renderForOpencode { - inherit pkgs; - canonical = cfg.agentsInput.lib.loadAgents; - modelOverrides = cfg.modelOverrides; - }; - }; + # Skills (merged from personal AGENTS repo + optional external skills) + xdg.configFile."opencode/skills" = let + cfg = config.coding.agents.opencode; + in + mkIf (cfg.agentsInput != null) { + source = cfg.agentsInput.lib.mkOpencodeSkills { + inherit pkgs; + customSkills = "${cfg.agentsInput}/skills"; + externalSkills = shared.mapExternalSkills cfg.externalSkills; + }; + }; - # Skills (merged from personal AGENTS repo + optional external skills) - xdg.configFile."opencode/skills" = mkIf (cfg.agentsInput != null) { - 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; - }; + # Static config dirs from AGENTS repo + xdg.configFile."opencode/context" = let + cfg = config.coding.agents.opencode; + in + mkIf (cfg.agentsInput != null) { + source = "${cfg.agentsInput}/context"; + }; + xdg.configFile."opencode/commands" = let + cfg = config.coding.agents.opencode; + in + mkIf (cfg.agentsInput != null) { + source = "${cfg.agentsInput}/commands"; + }; + xdg.configFile."opencode/prompts" = let + cfg = config.coding.agents.opencode; + in + mkIf (cfg.agentsInput != null) { + source = "${cfg.agentsInput}/prompts"; + }; }; - - # Static config dirs from AGENTS repo - xdg.configFile."opencode/context" = mkIf (cfg.agentsInput != null) { - source = "${cfg.agentsInput}/context"; - }; - xdg.configFile."opencode/commands" = mkIf (cfg.agentsInput != null) { - source = "${cfg.agentsInput}/commands"; - }; - xdg.configFile."opencode/prompts" = mkIf (cfg.agentsInput != null) { - source = "${cfg.agentsInput}/prompts"; - }; - }; -} + } diff --git a/modules/home-manager/coding/agents/pi.nix b/modules/home-manager/coding/agents/pi.nix index 0832f0b..d56f572 100644 --- a/modules/home-manager/coding/agents/pi.nix +++ b/modules/home-manager/coding/agents/pi.nix @@ -3,277 +3,225 @@ 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"; +}: let + shared = import ./shared-options.nix {inherit lib;}; +in + 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"; - 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 ~/.pi/agent/mcp.json. - Automatically inherits from config.programs.mcp.servers. - ''; - }; + 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 ~/.pi/agent/mcp.json. + Automatically inherits from config.programs.mcp.servers. + ''; + }; - agentsInput = mkOption { - type = types.nullOr types.anything; - default = null; - description = '' + agentsInput = shared.mkAgentsInputOption '' 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"; } - ''; - }; + modelOverrides = shared.mkModelOverridesOption; - 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. - ''; - }; - - 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.basecamp; } - ] - ''; - }; - - 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 ~/.pi/agent/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."; - }; - }; + 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. + ''; }; - default = {}; - description = '' - Pi settings written to ~/.pi/agent/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 - # 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 - ); + externalSkills = shared.externalSkillsOption; - piSettings = filterNulls cfg.settings; + 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 ~/.pi/agent/settings.json. + ''; + }; - # 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; + defaultProvider = mkOption { + type = types.nullOr types.str; + default = null; + description = "Default LLM provider (e.g. 'anthropic', 'openai', 'zai')."; + }; - # 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 = ".pi/agent/agents/${name}.md"; - value = {source = "${rendered}/agents/${name}.md";}; - }) - agentNames - ) - else {}; - in { - home.file = mkMerge [ - # ── MCP servers from programs.mcp → ~/.pi/agent/mcp.json ─────── - (mkIf (cfg.mcpServers != {}) { - ".pi/agent/mcp.json".text = builtins.toJSON {mcpServers = cfg.mcpServers;}; - }) + defaultModel = mkOption { + type = types.nullOr types.str; + default = null; + description = "Default model ID."; + }; - # ── ~/.pi/agent/settings.json ────────────────────────────────── - { - ".pi/agent/settings.json".text = builtins.toJSON piSettings; - } + defaultThinkingLevel = mkOption { + type = types.nullOr (types.enum ["off" "minimal" "low" "medium" "high" "xhigh"]); + default = null; + description = "Default extended thinking level."; + }; - # ── AGENTS.md — agent descriptions and specialist listing ────── - (mkIf (cfg.agentsInput != null) { - ".pi/agent/AGENTS.md".source = "${rendered}/AGENTS.md"; - }) + theme = mkOption { + type = types.nullOr types.str; + default = null; + description = "Pi theme name."; + }; - # ── SYSTEM.md — primary agent's system prompt ────────────────── - (mkIf (cfg.agentsInput != null) { - ".pi/agent/SYSTEM.md".source = "${rendered}/SYSTEM.md"; - }) + hideThinkingBlock = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Hide thinking blocks in output."; + }; - # ── Agents — pi-subagents .md files ──────────────────────────── - agentFiles + quietStartup = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Hide startup header."; + }; - # ── Skills symlinked from AGENTS repo + external skills ──────── - (mkIf (cfg.agentsInput != null) { - ".pi/agent/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; + 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 ~/.pi/agent/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 + # 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 = ".pi/agent/agents/${name}.md"; + value = {source = "${rendered}/agents/${name}.md";}; + }) + agentNames + ) + else {}; + in { + home.file = mkMerge [ + # ── MCP servers from programs.mcp → ~/.pi/agent/mcp.json ─────── + (mkIf (cfg.mcpServers != {}) { + ".pi/agent/mcp.json".text = builtins.toJSON {mcpServers = cfg.mcpServers;}; + }) + + # ── ~/.pi/agent/settings.json ────────────────────────────────── + { + ".pi/agent/settings.json".text = builtins.toJSON piSettings; + } + + # ── AGENTS.md — agent descriptions and specialist listing ────── + (mkIf (cfg.agentsInput != null) { + ".pi/agent/AGENTS.md".source = "${rendered}/AGENTS.md"; + }) + + # ── SYSTEM.md — primary agent's system prompt ────────────────── + (mkIf (cfg.agentsInput != null) { + ".pi/agent/SYSTEM.md".source = "${rendered}/SYSTEM.md"; + }) + + # ── Agents — pi-subagents .md files ──────────────────────────── + agentFiles + + # ── Skills symlinked from AGENTS repo + external skills ──────── + (mkIf (cfg.agentsInput != null) { + ".pi/agent/skills".source = cfg.agentsInput.lib.mkOpencodeSkills { + inherit pkgs; + customSkills = "${cfg.agentsInput}/skills"; + externalSkills = shared.mapExternalSkills cfg.externalSkills; + }; + }) + ]; + }); + } diff --git a/modules/home-manager/coding/agents/shared-options.nix b/modules/home-manager/coding/agents/shared-options.nix new file mode 100644 index 0000000..f10429f --- /dev/null +++ b/modules/home-manager/coding/agents/shared-options.nix @@ -0,0 +1,75 @@ +# Shared option definitions for agent modules. +# Prevents copy-pasting the externalSkills submodule across opencode/claude-code/pi. +{lib}: let + inherit (lib) mkOption mkEnableOption types literalExpression; +in { + # Common agentsInput option used by all agent modules. + mkAgentsInputOption = description: mkOption { + type = types.nullOr types.anything; + default = null; + inherit description; + }; + + # Common modelOverrides option. + mkModelOverridesOption = mkOption { + type = types.attrsOf types.str; + default = {}; + description = '' + Per-agent model overrides. Maps agent slug to model string. + Example: { chiron = "anthropic/claude-sonnet-4"; } + ''; + example = literalExpression '' + { + chiron = "anthropic/claude-sonnet-4"; + "chiron-forge" = "anthropic/claude-sonnet-4"; + } + ''; + }; + + # External skills submodule — used by opencode, claude-code, and pi modules. + externalSkillsOption = 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.basecamp; } + ] + ''; + }; + + # Helper to map externalSkills from module config to mkOpencodeSkills format. + mapExternalSkills = cfgEntries: + map ( + entry: + {inherit (entry) src skillsDir;} + // lib.optionalAttrs (entry.selectSkills != null) {inherit (entry) selectSkills;} + ) cfgEntries; +} diff --git a/overlays/default.nix b/overlays/default.nix deleted file mode 100644 index 574659c..0000000 --- a/overlays/default.nix +++ /dev/null @@ -1,24 +0,0 @@ -{inputs, ...}: { - # This one brings our custom packages from the 'pkgs' directory - additions = final: prev: (import ../pkgs {pkgs = final;}); - - # This one contains whatever you want to overlay - # You can change versions, add patches, set compilation flags, anything really. - # https://nixos.wiki/wiki/Overlays - modifications = final: prev: - # Import all package modifications from mods directory - (import ./mods/default.nix {inherit prev;}) - // { - # Direct configuration overrides - brave = prev.brave.override { - commandLineArgs = "--password-store=gnome-libsecret"; - }; - }; - - master-packages = final: _prev: { - master = import inputs.nixpkgs-master { - system = final.stdenv.hostPlatform.system; - config.allowUnfree = true; - }; - }; -} diff --git a/overlays/mods/beads.nix b/overlays/mods/beads.nix deleted file mode 100644 index 93acb16..0000000 --- a/overlays/mods/beads.nix +++ /dev/null @@ -1,16 +0,0 @@ -{prev}: -prev.beads.overrideAttrs (oldAttrs: rec { - version = "0.47.1"; - - src = prev.fetchFromGitHub { - owner = "steveyegge"; - repo = "beads"; - tag = "v${version}"; - hash = "sha256-DwIR/r1TJnpVd/CT1E2OTkAjU7k9/KHbcVwg5zziFVg="; - }; - - vendorHash = "sha256-pY5m5ODRgqghyELRwwxOr+xlW41gtJWLXaW53GlLaFw="; - - # Tests require git worktree operations that fail in Nix sandbox - doCheck = false; -}) diff --git a/overlays/mods/n8n.nix b/overlays/mods/n8n.nix deleted file mode 100644 index a0c24ba..0000000 --- a/overlays/mods/n8n.nix +++ /dev/null @@ -1,18 +0,0 @@ -{prev}: -prev.n8n.overrideAttrs (oldAttrs: rec { - version = "2.4.1"; - - src = prev.fetchFromGitHub { - owner = "n8n-io"; - repo = "n8n"; - rev = "n8n@${version}"; - hash = "sha256-EQP9ZI8kt30SUYE1+/UUpxQXpavzKqDu8qE24zsNifg="; - }; - - pnpmDeps = prev.pnpm_10.fetchDeps { - pname = oldAttrs.pname; - inherit version src; - fetcherVersion = 1; - hash = "sha256-Q30IuFEQD3896Hg0HCLd38YE2i8fJn74JY0o95LKJis="; - }; -})