feat: external skills

This commit is contained in:
sascha.koenig
2026-03-27 14:23:55 +01:00
parent a4ae041e1d
commit 586d1484ec
4 changed files with 309 additions and 47 deletions

View File

@@ -87,15 +87,61 @@ agents = {
``` ```
**Exports:** **Exports:**
- `lib.mkOpencodeSkills` — compose custom + external [skills.sh](https://skills.sh) skills into one directory
- `packages.skills-runtime` — composable runtime with all skill dependencies - `packages.skills-runtime` — composable runtime with all skill dependencies
- `devShells.default` — dev environment for working on skills - `devShells.default` — dev environment for working on skills
**Mapping** (via home-manager): **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 - `agents/agents.json` → embedded into config.json
- Agent changes: require `home-manager switch` - Agent changes: require `home-manager switch`
- Other changes: visible immediately - 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 ## Rules System
Centralized AI coding rules consumed via `mkOpencodeRules` from m3ta-nixpkgs: Centralized AI coding rules consumed via `mkOpencodeRules` from m3ta-nixpkgs:

View File

@@ -67,27 +67,20 @@ This repository is a **Nix flake** that exports:
- **`devShells.default`** — development environment for working on skills (activated via direnv) - **`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) - **`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:** **Consume in your system flake:**
```nix ```nix
# flake.nix # flake.nix
inputs.agents = { inputs = {
agents = {
url = "git+https://code.m3ta.dev/m3tam3re/AGENTS"; url = "git+https://code.m3ta.dev/m3tam3re/AGENTS";
inputs.nixpkgs.follows = "nixpkgs"; 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:** **Deploy skills via home-manager:**
@@ -95,15 +88,27 @@ programs.opencode.settings.agent = builtins.fromJSON
```nix ```nix
# home-manager module (e.g., opencode.nix) # home-manager module (e.g., opencode.nix)
{ inputs, system, ... }: { inputs, system, ... }:
{ let
# Skill files — symlinked, changes visible immediately pkgs = inputs.nixpkgs.legacyPackages.${system};
xdg.configFile = { in {
"opencode/skills".source = "${inputs.agents}/skills"; # Skills — composed from custom + external sources
"opencode/context".source = "${inputs.agents}/context"; xdg.configFile."opencode/skills".source =
"opencode/commands".source = "${inputs.agents}/commands"; inputs.agents.lib.mkOpencodeSkills {
"opencode/prompts".source = "${inputs.agents}/prompts"; 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) # Agent config — embedded into config.json (requires home-manager switch)
programs.opencode.settings.agent = builtins.fromJSON programs.opencode.settings.agent = builtins.fromJSON
(builtins.readFile "${inputs.agents}/agents/agents.json"); (builtins.readFile "${inputs.agents}/agents/agents.json");
@@ -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 ```nix
# Any project's flake.nix # Any project's flake.nix
{ {
inputs.agents.url = "git+https://code.m3ta.dev/m3tam3re/AGENTS"; inputs = {
inputs.agents.inputs.nixpkgs.follows = "nixpkgs"; 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 let
system = "x86_64-linux"; system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
in { 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 { devShells.${system}.default = pkgs.mkShell {
packages = [ packages = [
# project-specific tools
pkgs.nodejs pkgs.nodejs
# skill script dependencies
agents.packages.${system}.skills-runtime agents.packages.${system}.skills-runtime
]; ];
}; };
@@ -144,6 +164,8 @@ Rebuild:
home-manager switch 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`. **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 #### Option 2: Manual Installation

124
flake.nix
View File

@@ -7,13 +7,132 @@
let let
supportedSystems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ]; supportedSystems = [ "x86_64-linux" "aarch64-linux" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems; forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
inherit (nixpkgs) lib;
in { 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: # Usage:
# home.packages = [ inputs.agents.packages.${system}.skills-runtime ]; # home.packages = [ inputs.agents.packages.${system}.skills-runtime ];
# devShells.default = pkgs.mkShell { # devShells.default = pkgs.mkShell {
# packages = [ inputs.agents.packages.${system}.skills-runtime ]; # packages = [ inputs.agents.packages.${system}.skills-runtime ];
# }; # };
packages = forAllSystems (system: packages = forAllSystems (system:
let let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
@@ -49,7 +168,8 @@
}; };
}); });
# Dev shell for working on this repo (wraps skills-runtime). # ── Dev shell ──────────────────────────────────────────────────
devShells = forAllSystems (system: devShells = forAllSystems (system:
let let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};

View File

@@ -6,10 +6,15 @@
# ./scripts/test-skill.sh # List all development skills # ./scripts/test-skill.sh # List all development skills
# ./scripts/test-skill.sh <skill> # Validate specific skill # ./scripts/test-skill.sh <skill> # Validate specific skill
# ./scripts/test-skill.sh --run # Launch interactive opencode session # ./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 # This script creates a temporary XDG_CONFIG_HOME with symlinks to this
# repository's skills/, context/, command/, and prompts/ directories, # repository's skills/, context/, command/, and prompts/ directories,
# allowing you to test skill changes before deploying via home-manager. # 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 set -euo pipefail
@@ -21,12 +26,49 @@ GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
NC='\033[0m' NC='\033[0m'
EXTERNAL_SKILLS_DIRS=()
setup_test_config() { setup_test_config() {
local tmp_base="${TMPDIR:-/tmp}/opencode-test-$$" local tmp_base="${TMPDIR:-/tmp}/opencode-test-$$"
local tmp_config="$tmp_base/opencode" local tmp_config="$tmp_base/opencode"
local tmp_skills="$tmp_config/skills"
mkdir -p "$tmp_config" 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/context" "$tmp_config/context"
ln -sf "$REPO_ROOT/commands" "$tmp_config/commands" ln -sf "$REPO_ROOT/commands" "$tmp_config/commands"
ln -sf "$REPO_ROOT/prompts" "$tmp_config/prompts" ln -sf "$REPO_ROOT/prompts" "$tmp_config/prompts"
@@ -48,6 +90,7 @@ usage() {
echo " --run Launch interactive opencode session with dev skills" echo " --run Launch interactive opencode session with dev skills"
echo " --list List all skills (default if no args)" echo " --list List all skills (default if no args)"
echo " --validate Validate all skills" echo " --validate Validate all skills"
echo " --external <path> Add external skills directory (repeatable)"
echo " --help Show this help message" echo " --help Show this help message"
echo "" echo ""
echo "Arguments:" echo "Arguments:"
@@ -58,6 +101,7 @@ usage() {
echo " $0 task-management # Validate task-management skill" echo " $0 task-management # Validate task-management skill"
echo " $0 --validate # Validate all skills" echo " $0 --validate # Validate all skills"
echo " $0 --run # Launch interactive session" echo " $0 --run # Launch interactive session"
echo " $0 --run --external ~/src/anthropics-skills/skills"
} }
list_skills() { list_skills() {
@@ -127,25 +171,55 @@ run_opencode() {
XDG_CONFIG_HOME="$tmp_base" opencode XDG_CONFIG_HOME="$tmp_base" opencode
} }
# Main # Main — parse arguments
case "${1:-}" in ACTION=""
SKILL_NAME=""
while [[ $# -gt 0 ]]; do
case "$1" in
--help|-h) --help|-h)
usage usage
exit 0 exit 0
;; ;;
--run) --run)
run_opencode ACTION="run"
shift
;; ;;
--list) --list)
list_skills ACTION="list"
shift
;; ;;
--validate) --validate)
validate_all ACTION="validate"
shift
;; ;;
"") --external)
list_skills if [[ -z "${2:-}" ]]; then
echo -e "${RED}❌ --external requires a path argument${NC}" >&2
exit 1
fi
EXTERNAL_SKILLS_DIRS+=("$2")
shift 2
;; ;;
*) *)
validate_skill "$1" SKILL_NAME="$1"
ACTION="validate_one"
shift
;;
esac
done
case "${ACTION:-list}" in
run)
run_opencode
;;
list)
list_skills
;;
validate)
validate_all
;;
validate_one)
validate_skill "$SKILL_NAME"
;; ;;
esac esac