This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

1{ self, ... }: 2{ 3 flake.wrapperModules.claude-code = 4 { 5 config, 6 lib, 7 ... 8 }: 9 let 10 pkgs = config.pkgs; 11 jsonFormat = pkgs.formats.json { }; 12 13 # Assemble a single Claude Code plugin directory containing skills, 14 # commands, and agents. Claude Code discovers a directory as a plugin 15 # when it contains `.claude-plugin/plugin.json`. 16 pluginDir = 17 pkgs.runCommand "claude-plugin-${config.pluginName}" 18 { 19 passthru = { inherit (config) pluginManifest; }; 20 } 21 '' 22 mkdir -p $out/.claude-plugin 23 cp ${jsonFormat.generate "plugin.json" config.pluginManifest} \ 24 $out/.claude-plugin/plugin.json 25 26 ${lib.optionalString (config.skills != { }) '' 27 mkdir -p $out/skills 28 ${lib.concatStringsSep "\n" ( 29 lib.mapAttrsToList (name: src: "ln -s ${src} $out/skills/${name}") config.skills 30 )} 31 ''} 32 33 ${lib.optionalString (config.commands != { }) '' 34 mkdir -p $out/commands 35 ${lib.concatStringsSep "\n" ( 36 lib.mapAttrsToList (name: src: "ln -s ${src} $out/commands/${name}") config.commands 37 )} 38 ''} 39 40 ${lib.optionalString (config.agents != { }) '' 41 mkdir -p $out/agents 42 ${lib.concatStringsSep "\n" ( 43 lib.mapAttrsToList (name: src: "ln -s ${src} $out/agents/${name}") config.agents 44 )} 45 ''} 46 47 ${lib.optionalString (config.pluginExtraFiles != { }) '' 48 ${lib.concatStringsSep "\n" ( 49 lib.mapAttrsToList (path: src: '' 50 mkdir -p "$out/$(dirname ${path})" 51 ln -s ${src} "$out/${path}" 52 '') config.pluginExtraFiles 53 )} 54 ''} 55 ''; 56 57 # Compose a settings.json from the typed options + freeform overrides. 58 # Order matters: typed options take precedence over freeform `settings` 59 # for the keys they own, but freeform can still set anything else. 60 mergedSettings = lib.foldl' lib.recursiveUpdate config.settings ( 61 lib.optional (config.mcpServers != { }) { mcpServers = config.mcpServers; } 62 ++ lib.optional (config.hooks != { }) { hooks = config.hooks; } 63 ++ lib.optional (config.permissions != { }) { permissions = config.permissions; } 64 ); 65 66 settingsFile = jsonFormat.generate "claude-settings.json" mergedSettings; 67 68 levenshtein = self.packages.${pkgs.system}.levenshtein-merge; 69 in 70 { 71 _class = "wrapper"; 72 73 options = { 74 pluginName = lib.mkOption { 75 type = lib.types.str; 76 default = "superconfig"; 77 description = "Name of the bundled plugin directory."; 78 }; 79 80 pluginManifest = lib.mkOption { 81 type = jsonFormat.type; 82 default = { 83 name = config.pluginName; 84 version = "0.0.0"; 85 description = "Nix-managed Claude Code plugin bundle."; 86 }; 87 defaultText = lib.literalExpression '' 88 { name = config.pluginName; version = "0.0.0"; description = "..."; } 89 ''; 90 description = "Contents of `.claude-plugin/plugin.json` for the bundled plugin."; 91 }; 92 93 skills = lib.mkOption { 94 type = lib.types.attrsOf lib.types.path; 95 default = { }; 96 example = lib.literalExpression '' 97 { 98 websearch = ./skills/websearch; 99 } 100 ''; 101 description = '' 102 Skills to bundle. Each entry is a directory containing `SKILL.md` 103 and any helper scripts/resources. Symlinked into 104 `<plugin>/skills/<name>` and discovered by Claude Code automatically. 105 ''; 106 }; 107 108 commands = lib.mkOption { 109 type = lib.types.attrsOf lib.types.path; 110 default = { }; 111 description = '' 112 Slash commands. Each entry is a markdown file (or directory) 113 symlinked into `<plugin>/commands/<name>`. 114 ''; 115 }; 116 117 agents = lib.mkOption { 118 type = lib.types.attrsOf lib.types.path; 119 default = { }; 120 description = '' 121 Sub-agents. Each entry is a markdown file (or directory) symlinked 122 into `<plugin>/agents/<name>`. 123 ''; 124 }; 125 126 pluginExtraFiles = lib.mkOption { 127 type = lib.types.attrsOf lib.types.path; 128 default = { }; 129 example = lib.literalExpression '' 130 { 131 "hooks/format.sh" = ./hooks/format.sh; 132 } 133 ''; 134 description = '' 135 Additional files to drop into the plugin directory. Keys are 136 relative paths inside the plugin directory; values are paths to 137 symlink in. Use this for hook scripts or anything else not covered 138 by the typed options above. 139 ''; 140 }; 141 142 mcpServers = lib.mkOption { 143 type = jsonFormat.type; 144 default = { }; 145 example = lib.literalExpression '' 146 { 147 git-mcp = { 148 command = "''${pkgs.git-mcp}/bin/git-mcp"; 149 }; 150 tmux-mcp = { 151 type = "stdio"; 152 command = "''${pkgs.tmux-mcp}/bin/tmux-mcp"; 153 args = [ ]; 154 env = { }; 155 }; 156 } 157 ''; 158 description = '' 159 MCP servers to register. Merged into `settings.json` under 160 `mcpServers`. Each entry follows Claude Code's MCP server schema 161 (stdio: `{ command, args, env }`; http/sse: `{ type, url, headers }`). 162 ''; 163 }; 164 165 hooks = lib.mkOption { 166 type = jsonFormat.type; 167 default = { }; 168 example = lib.literalExpression '' 169 { 170 PreToolUse = [ 171 { 172 matcher = "*"; 173 hooks = [ 174 { 175 type = "command"; 176 command = "''${pkgs.my-permission-bridge}/bin/permission-forward"; 177 } 178 ]; 179 } 180 ]; 181 } 182 ''; 183 description = '' 184 Hook definitions. Shape matches Claude Code's `settings.json` hooks 185 field. Supported events include `PreToolUse`, `PostToolUse`, 186 `UserPromptSubmit`, `Stop`, `SessionStart`, `Notification`, and 187 `PreCompact`. 188 ''; 189 }; 190 191 permissions = lib.mkOption { 192 type = jsonFormat.type; 193 default = { }; 194 example = lib.literalExpression '' 195 { 196 defaultMode = "acceptEdits"; 197 allow = [ 198 "Bash(nix build:*)" 199 "Bash(nix eval:*)" 200 "Bash(nix fmt:*)" 201 "Bash(git status:*)" 202 "Bash(git diff:*)" 203 ]; 204 deny = [ ]; 205 ask = [ ]; 206 } 207 ''; 208 description = '' 209 Permission rules. Shape matches Claude Code's `settings.json` 210 `permissions` field. Use `allow`/`deny`/`ask` pattern lists to 211 short-circuit the permission flow before any hook fires. 212 ''; 213 }; 214 215 settings = lib.mkOption { 216 type = jsonFormat.type; 217 default = { }; 218 description = '' 219 Freeform additional `settings.json` content. Merged with the 220 typed options above (which take precedence on conflict). Use this 221 for anything not covered by a dedicated option. 222 ''; 223 }; 224 225 claudeMd = lib.mkOption { 226 type = lib.types.nullOr lib.types.path; 227 default = null; 228 description = '' 229 Optional `CLAUDE.md` whose lines are fuzzy-merged into 230 `$CLAUDE_MD_DIR/CLAUDE.md` (default `~/.claude/CLAUDE.md`) on 231 every invocation. Only adds lines, never removes user content. 232 Each non-blank managed line is skipped when a line within 233 `claudeMdMergeThreshold` (Levenshtein ratio) already exists in 234 the target file. 235 ''; 236 }; 237 238 claudeMdMergeThreshold = lib.mkOption { 239 type = lib.types.float; 240 default = 0.15; 241 description = '' 242 Levenshtein distance ratio (distance / max-length) below which 243 two normalized lines are considered the same during 244 `claudeMd` merging. 0.0 requires exact match; higher values 245 tolerate more typos. Default 0.15 allows roughly one typo per 246 seven characters. 247 ''; 248 }; 249 250 pluginDir = lib.mkOption { 251 type = lib.types.package; 252 readOnly = true; 253 default = pluginDir; 254 defaultText = "<assembled plugin directory>"; 255 description = "The assembled plugin directory passed via `--plugin-dir`."; 256 }; 257 258 settingsFile = lib.mkOption { 259 type = lib.types.package; 260 readOnly = true; 261 default = settingsFile; 262 defaultText = "<assembled settings.json>"; 263 description = "The assembled settings.json passed via `--settings`."; 264 }; 265 }; 266 267 config.package = lib.mkDefault (self.lib.halalify pkgs.claude-code); 268 269 # Always inject our plugin dir + settings file into every invocation. 270 # `--plugin-dir` is repeatable so users can still pass more on the CLI. 271 config.flags."--plugin-dir" = { 272 value = [ "${pluginDir}" ]; 273 order = 100; 274 }; 275 config.flags."--settings" = { 276 value = "${settingsFile}"; 277 order = 101; 278 }; 279 280 config.preHook = lib.optionalString (config.claudeMd != null) '' 281 CLAUDE_MD_DIR="''${CLAUDE_MD_DIR:-$HOME/.claude}" 282 mkdir -p "$CLAUDE_MD_DIR" 283 touch "$CLAUDE_MD_DIR/CLAUDE.md" 284 ${levenshtein}/bin/levenshtein-merge merge \ 285 ${config.claudeMd} \ 286 "$CLAUDE_MD_DIR/CLAUDE.md" \ 287 ${toString config.claudeMdMergeThreshold} \ 288 || echo "levenshtein merge failed; leaving CLAUDE.md untouched" >&2 289 ''; 290 291 config.meta.maintainers = [ 292 { 293 name = "lassulus"; 294 github = "lassulus"; 295 githubId = 621375; 296 } 297 ]; 298 config.meta.platforms = lib.platforms.all; 299 }; 300}