Editor and AI batteries
An editor’s extension list grows with every language you use, and every AI agent wants the same skills and MCP
servers in its own format. Here a language aspect gives what it needs once. Each editor and
each agent reads from one pool. The example is one standalone home, ana, built with home-manager.
# Standalone homes. Language aspects add editor extensions and settings; AI tools add agent extensions.{ yen, lib, inputs, ...}:let b = yen.lib.batteries;in{ yen.kinds.fleet = { spawns.home = _: yen.home; sinks.out.reduce = yen.lib.reduce.merge; }; yen.kinds.home = { sinks = { homeManager.out = b.home-manager.system { # (1) from = yen.kinds.home.sinks.homeManager; homeManagerConfiguration = _: inputs.home-manager.lib.homeManagerConfiguration; pkgs = _: inputs.nixpkgs.legacyPackages.x86_64-linux; }; vscode.homeManager = b.editors.vscode { from = yen.kinds.home.sinks.vscode; }; # (2) zed.homeManager = b.editors.zed { from = yen.kinds.home.sinks.zed; }; # (3) # (4) one pool of agent extensions; each agent picks the types it reads agent-ext.homeManager = b.ai.extensions { from = yen.kinds.home.sinks.agent-ext; types = [ "skill" ]; build = skills: { home.file = lib.listToAttrs ( map (s: lib.nameValuePair ".claude/skills/${s.name}" { source = s.path; }) skills ); }; }; }; }; yen.fleet.main = { }; yen.home.ana = { }; yen.flake = { out, ... }: out; yen.aspects.home = { at = yen.kinds.home; homeManager = { scope, ... }: { home.username = scope.name; home.homeDirectory = "/home/${scope.name}"; home.stateVersion = "25.05"; nixpkgs.config.allowUnfree = true; # vscode }; };}The home kind has one sink for each editor and one for the agents. The batteries are the values of the sinks’ homeManager keys.
(1) builds the home. (2) turns the vscode sink into programs.vscode. (3) does the same for Zed, with
extensions by name. At (4), agent-ext is the pool of agent extensions. types says which records this agent reads:
here, only skills.
Each language aspect gives its part to the editors, so no editor file lists the languages:
# Each language aspect gives what both editors need; no editor file lists the languages.{ yen, inputs, ... }:let pkgs = inputs.nixpkgs.legacyPackages.x86_64-linux;in{ yen.aspects.nix-lang = { at = yen.kinds.home; vscode = { # (1) extensions = [ pkgs.vscode-extensions.jnoortheen.nix-ide ]; settings."nix.enableLanguageServer" = true; }; zed.extensions = [ "nix" ]; # (2) }; yen.aspects.go-lang = { at = yen.kinds.home; vscode.extensions = [ pkgs.vscode-extensions.golang.go ]; zed.extensions = [ "go" ]; }; # skills from a directory, and an mcp server, in one pool yen.aspects.agents = { at = yen.kinds.home; agent-ext = yen.lib.batteries.ai.skills ../skills ++ [ # (3) { type = "mcp"; name = "github"; } ]; };}(1) gives VS Code a list of extensions and its settings. (2) gives Zed the same language by name. At (3),
ai.skills reads a directory and gives one record for each subdirectory that has a SKILL.md. The aspect
adds an MCP server record to the same pool. This is the skill it finds:
---name: reviewdescription: review a diff---Read the diff. List the risks.The directory notes has no SKILL.md, so it is not a skill. The github record has the type mcp, so the skills agent skips it.
The batteries
Section titled “The batteries”| Battery | Value of | Gives |
|---|---|---|
editors.vscode { from; profile; } |
a home-manager key of a sink of { extensions; settings; } |
programs.vscode |
editors.zed { from; } |
the same, extensions by name | programs.zed-editor |
ai.extensions { from; types; build; } |
any key | build of the records of the given types (an element may be a list) |
ai.skills dir |
an element | one { type = "skill"; name; path; } per subdirectory with a SKILL.md |