{ description = "Opencode Agent Skills — development environment & runtime"; inputs = {nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";}; outputs = { self, nixpkgs, }: let supportedSystems = ["x86_64-linux" "aarch64-linux" "aarch64-darwin"]; forAllSystems = nixpkgs.lib.genAttrs supportedSystems; inherit (nixpkgs) lib; in { # ── Skill composition library ────────────────────────────────── # # Merges custom skills with external skills.sh sources into a # single directory suitable for ~/.config/opencode/skills or # .agents/skills in project flakes. # # Usage (home-manager): # xdg.configFile."opencode/skills".source = # inputs.agents.lib.mkOpencodeSkills { # pkgs = nixpkgs.legacyPackages.${system}; # customSkills = "${inputs.agents}/skills"; # externalSkills = [ # { src = inputs.skills-anthropic; } # { src = inputs.skills-vercel; selectSkills = [ "find-skills" ]; } # ]; # }; # # Usage (project flake — project-level skills): # ".agents/skills".source = # inputs.agents.lib.mkOpencodeSkills { # pkgs = nixpkgs.legacyPackages.${system}; # externalSkills = [ # { src = inputs.skills-anthropic; selectSkills = [ "mcp-builder" ]; } # ]; # }; # # Parameters: # pkgs — nixpkgs package set (required) # customSkills — path to a directory of skill subdirectories (optional) # externalSkills — list of external skill sources (optional, default []) # Each element is an attrset: # src — path to repo root (flake input or local path) # skillsDir — subdirectory containing skills (default "skills") # selectSkills — list of skill names to include (default: all) # # Collision handling: # Custom skills always take priority over external ones. # Among external sources, earlier entries in the list take priority. lib.mkOpencodeSkills = { pkgs, customSkills ? null, externalSkills ? [], }: let # Resolve a single external source into a list of { name, path } entries. resolveExternal = entry: let skillsRoot = "${entry.src}/${entry.skillsDir or "skills"}"; # List skill subdirectories (each must contain SKILL.md). allSkillDirs = lib.pipe (builtins.readDir skillsRoot) [ (lib.filterAttrs (_: type: type == "directory")) (dirs: lib.attrNames dirs) ]; selected = if entry ? selectSkills then builtins.filter (name: builtins.elem name entry.selectSkills) allSkillDirs else allSkillDirs; in map (name: { inherit name; path = "${skillsRoot}/${name}"; }) selected; # Collect all external skills, flattened. allExternal = lib.concatMap resolveExternal externalSkills; # Collect custom skill names for collision detection. customSkillNames = if customSkills != null then lib.attrNames (lib.filterAttrs (_: type: type == "directory") (builtins.readDir customSkills)) else []; # Filter out external skills that collide with custom ones. # Among externals, keep first occurrence (earlier sources win). filterExternals = externals: let go = acc: remaining: if remaining == [] then acc.result else let head = builtins.head remaining; tail = builtins.tail remaining; isDuplicate = builtins.elem head.name acc.seen; in if isDuplicate then go acc tail else go { seen = acc.seen ++ [head.name]; result = acc.result ++ [head]; } tail; in go { seen = customSkillNames; result = []; } externals; filteredExternal = filterExternals allExternal; # Build a linkFarm entry for each external skill. externalLinks = map (skill: { name = skill.name; path = skill.path; }) filteredExternal; # Build a linkFarm entry for each custom skill. customLinks = if customSkills != null then map (name: { inherit name; path = "${customSkills}/${name}"; }) customSkillNames else []; in pkgs.linkFarm "opencode-skills" (customLinks ++ externalLinks); # ── Agent loader ─────────────────────────────────────────────── # # Reads all canonical agents/*/agent.toml + agents/*/system-prompt.md # files and returns an attrset keyed by agent slug. # # Each value has all fields from agent.toml plus: # systemPrompt — full content of system-prompt.md # # Usage: # inputs.agents.lib.loadAgents.chiron.description # inputs.agents.lib.loadAgents.chiron.systemPrompt lib.loadAgents = let agentDirs = builtins.attrNames ( lib.filterAttrs (_: t: t == "directory") (builtins.readDir ./agents) ); isAgentDir = name: builtins.pathExists ./agents/${name}/agent.toml; loadAgent = name: (builtins.fromTOML (builtins.readFile ./agents/${name}/agent.toml)) // {systemPrompt = builtins.readFile ./agents/${name}/system-prompt.md;}; in builtins.listToAttrs ( map (name: { inherit name; value = loadAgent name; }) (builtins.filter isAgentDir agentDirs) ); # ── Backward-compat agents.json bridge ──────────────────────── # # Produces an attrset semantically equivalent to agents/agents.json, # keyed by display name (e.g. "Chiron (Assistant)"). # # Suitable for embedding into opencode config.json via home-manager: # programs.opencode.settings.agent = inputs.agents.lib.agentsJson; # # Shape per agent: # description — agent purpose string # mode — "primary" | "subagent" # model — LLM model ID (fixed: "zai-coding-plan/glm-5") # permission — reconstructed permission object # prompt — "{file:./prompts/.txt}" lib.agentsJson = let model = "zai-coding-plan/glm-5"; # Convert a single permission section from canonical TOML two-level # (intent + rules[]) into the JSON nested object shape. # intent-only → simple string # intent+rules → { "*": intent, pattern: action, ... } renderPermSection = section: if !(section ? rules) || section.rules == [] then section.intent else let # Parse "pattern:action" — split on first colon only. parseRule = ruleStr: let colonIdx = lib.strings.stringLength ( builtins.head (lib.strings.splitString ":" ruleStr) ); pattern = builtins.substring 0 colonIdx ruleStr; action = builtins.substring (colonIdx + 1) (lib.strings.stringLength ruleStr) ruleStr; in { name = pattern; value = action; }; ruleAttrs = builtins.listToAttrs (map parseRule section.rules); in {"*" = section.intent;} // ruleAttrs; # Convert canonical permissions attrset to JSON permission object. renderPermissions = perms: builtins.mapAttrs (_: renderPermSection) perms; # Build one agent entry in the agentsJson shape. renderAgent = slug: agent: { description = agent.description + "."; mode = agent.mode; model = model; permission = renderPermissions agent.permissions; prompt = "{file:./prompts/${slug}.txt}"; }; agents = self.lib.loadAgents; in builtins.listToAttrs ( map (slug: let agent = builtins.getAttr slug agents; in { name = agent.display_name; value = renderAgent slug agent; }) (builtins.attrNames agents) ); # ── Composable runtime ───────────────────────────────────────── # # Runtime dependencies for skill scripts (Python packages, system # tools). Include in home.packages or project devShells. # # Usage: # home.packages = [ inputs.agents.packages.${system}.skills-runtime ]; # devShells.default = pkgs.mkShell { # packages = [ inputs.agents.packages.${system}.skills-runtime ]; # }; packages = forAllSystems (system: let pkgs = nixpkgs.legacyPackages.${system}; pythonEnv = pkgs.python3.withPackages (ps: with ps; [ # skill-creator: quick_validate.py pyyaml # xlsx: recalc.py openpyxl # prompt-engineering-patterns: optimize-prompt.py numpy # pdf: multiple scripts pypdf pillow # PIL pdf2image # excalidraw: render_excalidraw.py playwright ]); in { skills-runtime = pkgs.buildEnv { name = "opencode-skills-runtime"; paths = [ pythonEnv pkgs.poppler-utils # pdf: pdftoppm/pdfinfo pkgs.jq # shell scripts pkgs.playwright-driver.browsers # excalidraw: chromium for rendering ]; }; }); # ── Dev shell ────────────────────────────────────────────────── devShells = forAllSystems (system: let pkgs = nixpkgs.legacyPackages.${system}; in { default = pkgs.mkShell { packages = [self.packages.${system}.skills-runtime]; env.PLAYWRIGHT_BROWSERS_PATH = "${pkgs.playwright-driver.browsers}"; shellHook = '' echo "🔧 AGENTS dev shell active — Python $(python3 --version 2>&1 | cut -d' ' -f2), $(jq --version)" ''; }; }); }; }