From 586d1484ec50629ebabe6e3c1e52b8aac4a6a9fb Mon Sep 17 00:00:00 2001 From: "sascha.koenig" Date: Fri, 27 Mar 2026 14:23:55 +0100 Subject: [PATCH] feat: external skills --- AGENTS.md | 48 +++++++++++++++- README.md | 80 +++++++++++++++++---------- flake.nix | 124 +++++++++++++++++++++++++++++++++++++++++- scripts/test-skill.sh | 104 ++++++++++++++++++++++++++++++----- 4 files changed, 309 insertions(+), 47 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a3dd42c..4b19258 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,15 +87,61 @@ agents = { ``` **Exports:** +- `lib.mkOpencodeSkills` — compose custom + external [skills.sh](https://skills.sh) skills into one directory - `packages.skills-runtime` — composable runtime with all skill dependencies - `devShells.default` — dev environment for working on skills **Mapping** (via home-manager): -- `skills/`, `context/`, `commands/`, `prompts/` → symlinks +- `skills/` → composed via `mkOpencodeSkills` (custom + external merged) +- `context/`, `commands/`, `prompts/` → symlinks - `agents/agents.json` → embedded into config.json - Agent changes: require `home-manager switch` - Other changes: visible immediately +### External Skills (skills.sh) + +This repo supports composing skills from external [skills.sh](https://skills.sh) repositories +alongside custom skills. External repos follow the [Agent Skills](https://agentskills.io) +standard (same `SKILL.md` format). + +**`lib.mkOpencodeSkills` parameters:** +- `pkgs` (required) — nixpkgs package set +- `customSkills` (optional) — path to custom skills directory (e.g., `"${inputs.agents}/skills"`) +- `externalSkills` (optional) — list of external sources, each with: + - `src` — flake input or path to repo root + - `skillsDir` — subdirectory containing skills (default: `"skills"`) + - `selectSkills` — list of skill names to include (default: all) + +**Collision handling:** Custom skills always win. Among externals, earlier entries take priority. + +**Home-manager example:** +```nix +inputs = { + agents.url = "git+https://code.m3ta.dev/m3tam3re/AGENTS"; + skills-anthropic = { url = "github:anthropics/skills"; flake = false; }; +}; + +xdg.configFile."opencode/skills".source = + inputs.agents.lib.mkOpencodeSkills { + pkgs = nixpkgs.legacyPackages.${system}; + customSkills = "${inputs.agents}/skills"; + externalSkills = [ + { src = inputs.skills-anthropic; } + ]; + }; +``` + +**Project flake example (selective):** +```nix +".agents/skills".source = + inputs.agents.lib.mkOpencodeSkills { + pkgs = nixpkgs.legacyPackages.${system}; + externalSkills = [ + { src = inputs.skills-anthropic; selectSkills = [ "mcp-builder" ]; } + ]; + }; +``` + ## Rules System Centralized AI coding rules consumed via `mkOpencodeRules` from m3ta-nixpkgs: diff --git a/README.md b/README.md index 77babdf..a5e7fed 100644 --- a/README.md +++ b/README.md @@ -67,27 +67,20 @@ This repository is a **Nix flake** that exports: - **`devShells.default`** — development environment for working on skills (activated via direnv) - **`packages.skills-runtime`** — composable runtime with all skill script dependencies (Python packages + system tools) +- **`lib.mkOpencodeSkills`** — compose custom skills with external [skills.sh](https://skills.sh) skills into a single directory **Consume in your system flake:** ```nix # flake.nix -inputs.agents = { - url = "git+https://code.m3ta.dev/m3tam3re/AGENTS"; - inputs.nixpkgs.follows = "nixpkgs"; +inputs = { + agents = { + url = "git+https://code.m3ta.dev/m3tam3re/AGENTS"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + # Optional: add external skill repositories (skills.sh / agentskills.io) + skills-anthropic = { url = "github:anthropics/skills"; flake = false; }; }; - -# In your home-manager module (e.g., opencode.nix) -xdg.configFile = { - "opencode/skills".source = "${inputs.agents}/skills"; - "opencode/context".source = "${inputs.agents}/context"; - "opencode/commands".source = "${inputs.agents}/commands"; - "opencode/prompts".source = "${inputs.agents}/prompts"; -}; - -# Agent config is embedded into config.json, not deployed as files -programs.opencode.settings.agent = builtins.fromJSON - (builtins.readFile "${inputs.agents}/agents/agents.json"); ``` **Deploy skills via home-manager:** @@ -95,14 +88,26 @@ programs.opencode.settings.agent = builtins.fromJSON ```nix # home-manager module (e.g., opencode.nix) { inputs, system, ... }: -{ - # Skill files — symlinked, changes visible immediately - xdg.configFile = { - "opencode/skills".source = "${inputs.agents}/skills"; - "opencode/context".source = "${inputs.agents}/context"; - "opencode/commands".source = "${inputs.agents}/commands"; - "opencode/prompts".source = "${inputs.agents}/prompts"; - }; +let + pkgs = inputs.nixpkgs.legacyPackages.${system}; +in { + # Skills — composed from custom + external sources + xdg.configFile."opencode/skills".source = + inputs.agents.lib.mkOpencodeSkills { + inherit pkgs; + customSkills = "${inputs.agents}/skills"; + externalSkills = [ + # Include all skills from anthropics/skills + { src = inputs.skills-anthropic; } + # Or cherry-pick specific skills: + # { src = inputs.skills-anthropic; selectSkills = [ "mcp-builder" ]; } + ]; + }; + + # Other config — symlinked directly + xdg.configFile."opencode/context".source = "${inputs.agents}/context"; + xdg.configFile."opencode/commands".source = "${inputs.agents}/commands"; + xdg.configFile."opencode/prompts".source = "${inputs.agents}/prompts"; # Agent config — embedded into config.json (requires home-manager switch) programs.opencode.settings.agent = builtins.fromJSON @@ -113,24 +118,39 @@ programs.opencode.settings.agent = builtins.fromJSON } ``` -**Compose into project flakes** (so opencode has skill deps in any project): +> **Note**: If you don't use external skills, you can still use the simple form: +> ```nix +> xdg.configFile."opencode/skills".source = "${inputs.agents}/skills"; +> ``` + +**Compose into project flakes** (project-level skills for a specific repo): ```nix # Any project's flake.nix { - inputs.agents.url = "git+https://code.m3ta.dev/m3tam3re/AGENTS"; - inputs.agents.inputs.nixpkgs.follows = "nixpkgs"; + inputs = { + agents.url = "git+https://code.m3ta.dev/m3tam3re/AGENTS"; + agents.inputs.nixpkgs.follows = "nixpkgs"; + skills-anthropic = { url = "github:anthropics/skills"; flake = false; }; + }; - outputs = { self, nixpkgs, agents, ... }: + outputs = { self, nixpkgs, agents, skills-anthropic, ... }: let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; in { + # Project-level skills (placed in .agents/skills/) + ".agents/skills".source = + agents.lib.mkOpencodeSkills { + inherit pkgs; + externalSkills = [ + { src = skills-anthropic; selectSkills = [ "mcp-builder" ]; } + ]; + }; + devShells.${system}.default = pkgs.mkShell { packages = [ - # project-specific tools pkgs.nodejs - # skill script dependencies agents.packages.${system}.skills-runtime ]; }; @@ -144,6 +164,8 @@ Rebuild: home-manager switch ``` +**Collision handling**: When a custom skill and an external skill share the same name, the custom skill always wins. Among external sources, earlier entries in the `externalSkills` list take priority. + **Note**: The `agents/` directory is NOT deployed as files. Instead, `agents.json` is read at Nix evaluation time and embedded into the opencode `config.json`. #### Option 2: Manual Installation diff --git a/flake.nix b/flake.nix index 5b7d66a..2730105 100644 --- a/flake.nix +++ b/flake.nix @@ -7,13 +7,132 @@ let supportedSystems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ]; forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + inherit (nixpkgs) lib; in { - # Composable runtime for project flakes and home-manager. + + # ── 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); + + # ── 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}; @@ -49,7 +168,8 @@ }; }); - # Dev shell for working on this repo (wraps skills-runtime). + # ── Dev shell ────────────────────────────────────────────────── + devShells = forAllSystems (system: let pkgs = nixpkgs.legacyPackages.${system}; diff --git a/scripts/test-skill.sh b/scripts/test-skill.sh index e499613..3c951f7 100755 --- a/scripts/test-skill.sh +++ b/scripts/test-skill.sh @@ -6,10 +6,15 @@ # ./scripts/test-skill.sh # List all development skills # ./scripts/test-skill.sh # Validate specific skill # ./scripts/test-skill.sh --run # Launch interactive opencode session +# ./scripts/test-skill.sh --run --external /path/to/skills-repo/skills # # This script creates a temporary XDG_CONFIG_HOME with symlinks to this # repository's skills/, context/, command/, and prompts/ directories, # allowing you to test skill changes before deploying via home-manager. +# +# The --external flag allows testing with external skills.sh repositories. +# Point it to the skills/ subdirectory of a cloned skills.sh repo. +# Custom skills take priority over external ones with the same name. set -euo pipefail @@ -21,12 +26,49 @@ GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' +EXTERNAL_SKILLS_DIRS=() + setup_test_config() { local tmp_base="${TMPDIR:-/tmp}/opencode-test-$$" local tmp_config="$tmp_base/opencode" + local tmp_skills="$tmp_config/skills" mkdir -p "$tmp_config" - ln -sf "$REPO_ROOT/skills" "$tmp_config/skills" + + if [[ ${#EXTERNAL_SKILLS_DIRS[@]} -gt 0 ]]; then + # Merge mode: create a real directory with symlinks to individual skills. + # Custom skills take priority (linked first), externals fill gaps. + mkdir -p "$tmp_skills" + + # Link custom skills first (they always win). + for skill_dir in "$REPO_ROOT/skills/"*/; do + local skill_name + skill_name=$(basename "$skill_dir") + ln -sf "$skill_dir" "$tmp_skills/$skill_name" + done + + # Link external skills (skip if name already exists from custom). + for ext_dir in "${EXTERNAL_SKILLS_DIRS[@]}"; do + if [[ ! -d "$ext_dir" ]]; then + echo -e "${RED}❌ External skills directory not found: $ext_dir${NC}" >&2 + continue + fi + for skill_dir in "$ext_dir/"*/; do + [[ -d "$skill_dir" ]] || continue + local skill_name + skill_name=$(basename "$skill_dir") + if [[ ! -e "$tmp_skills/$skill_name" ]]; then + ln -sf "$skill_dir" "$tmp_skills/$skill_name" + else + echo -e "${YELLOW} ⚠ Skipping external '$skill_name' (custom takes priority)${NC}" >&2 + fi + done + done + else + # Simple mode: symlink entire directory. + ln -sf "$REPO_ROOT/skills" "$tmp_skills" + fi + ln -sf "$REPO_ROOT/context" "$tmp_config/context" ln -sf "$REPO_ROOT/commands" "$tmp_config/commands" ln -sf "$REPO_ROOT/prompts" "$tmp_config/prompts" @@ -48,6 +90,7 @@ usage() { echo " --run Launch interactive opencode session with dev skills" echo " --list List all skills (default if no args)" echo " --validate Validate all skills" + echo " --external Add external skills directory (repeatable)" echo " --help Show this help message" echo "" echo "Arguments:" @@ -58,6 +101,7 @@ usage() { echo " $0 task-management # Validate task-management skill" echo " $0 --validate # Validate all skills" echo " $0 --run # Launch interactive session" + echo " $0 --run --external ~/src/anthropics-skills/skills" } list_skills() { @@ -127,25 +171,55 @@ run_opencode() { XDG_CONFIG_HOME="$tmp_base" opencode } -# Main -case "${1:-}" in - --help|-h) - usage - exit 0 - ;; - --run) +# Main — parse arguments +ACTION="" +SKILL_NAME="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --help|-h) + usage + exit 0 + ;; + --run) + ACTION="run" + shift + ;; + --list) + ACTION="list" + shift + ;; + --validate) + ACTION="validate" + shift + ;; + --external) + if [[ -z "${2:-}" ]]; then + echo -e "${RED}❌ --external requires a path argument${NC}" >&2 + exit 1 + fi + EXTERNAL_SKILLS_DIRS+=("$2") + shift 2 + ;; + *) + SKILL_NAME="$1" + ACTION="validate_one" + shift + ;; + esac +done + +case "${ACTION:-list}" in + run) run_opencode ;; - --list) + list) list_skills ;; - --validate) + validate) validate_all ;; - "") - list_skills - ;; - *) - validate_skill "$1" + validate_one) + validate_skill "$SKILL_NAME" ;; esac