From 808a5b5dbf4f78fabefb36ce5d0f6b33c34c6f84 Mon Sep 17 00:00:00 2001 From: Gleb Khaykin Date: Sun, 16 Aug 2026 20:55:50 +0200 Subject: [PATCH 1/2] feat(agents): scope work config and add local Grafana MCP --- README.md | 2 - users/gkhaykin/default.nix | 3 +- users/gkhaykin/onepassword.nix | 1 + users/gkhaykin/programs/agents/mcp.nix | 32 ++++++ .../skills/linear-ticket-writing/SKILL.md | 0 .../skills/slack-pr-announcement/SKILL.md | 0 .../skills/weekly-cycle-update/SKILL.md | 0 users/gkhaykin/programs/claude.nix | 37 +++++++ users/gkhaykin/programs/cursor.nix | 19 ++++ users/gkhaykin/programs/default.nix | 9 ++ users/shared/programs/agents/default.nix | 15 --- users/shared/programs/agents/mcp-servers.nix | 103 ------------------ users/shared/programs/agents/rules.nix | 22 +--- users/shared/programs/agents/skills.nix | 18 +++ users/shared/programs/claude.nix | 37 +++---- users/shared/programs/cursor/agents.nix | 19 ++++ .../{cursor.nix => cursor/default.nix} | 12 +- users/shared/programs/default.nix | 27 +++-- 18 files changed, 178 insertions(+), 178 deletions(-) create mode 100644 users/gkhaykin/programs/agents/mcp.nix rename users/{shared => gkhaykin}/programs/agents/skills/linear-ticket-writing/SKILL.md (100%) rename users/{shared => gkhaykin}/programs/agents/skills/slack-pr-announcement/SKILL.md (100%) rename users/{shared => gkhaykin}/programs/agents/skills/weekly-cycle-update/SKILL.md (100%) create mode 100644 users/gkhaykin/programs/claude.nix create mode 100644 users/gkhaykin/programs/cursor.nix create mode 100644 users/gkhaykin/programs/default.nix delete mode 100644 users/shared/programs/agents/default.nix delete mode 100644 users/shared/programs/agents/mcp-servers.nix create mode 100644 users/shared/programs/agents/skills.nix create mode 100644 users/shared/programs/cursor/agents.nix rename users/shared/programs/{cursor.nix => cursor/default.nix} (88%) diff --git a/README.md b/README.md index dae7830..14ac997 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,6 @@ Declarative and reproducible environment across machines: - Homebrew integration via `nix-homebrew` - Cursor and Claude Code configurations from shared definitions -Agent rules, skills, and MCP servers are defined once in `users/shared/programs/agents`. Nix renders them into the per-client format each tool expects, so Cursor and Claude Code stay in sync. - ## Layout | Path | Contents | diff --git a/users/gkhaykin/default.nix b/users/gkhaykin/default.nix index f4443ab..1f3be70 100644 --- a/users/gkhaykin/default.nix +++ b/users/gkhaykin/default.nix @@ -7,8 +7,7 @@ imports = [ ../shared/programs ./onepassword.nix - ./programs/finicky.nix - ./programs/vscode + ./programs ../shared/completions.nix ../shared/fonts.nix diff --git a/users/gkhaykin/onepassword.nix b/users/gkhaykin/onepassword.nix index 39d7e8e..0d668ed 100644 --- a/users/gkhaykin/onepassword.nix +++ b/users/gkhaykin/onepassword.nix @@ -9,6 +9,7 @@ let onePasswordEnvironmentVariables = { ANTHROPIC_API_KEY = "op://Employee/Anthropic/credential"; + GRAFANA_SERVICE_ACCOUNT_TOKEN = "op://Employee/Grafana MCP Service Account/token"; NETBOX_TOKEN = "op://Employee/NetBox/credential"; TOGETHER_PROD_API_KEY = "op://Employee/Together Prod/public_credential"; TOGETHER_PROD_INTERNAL_API_KEY = "op://Employee/Together Prod/internal_credential"; diff --git a/users/gkhaykin/programs/agents/mcp.nix b/users/gkhaykin/programs/agents/mcp.nix new file mode 100644 index 0000000..2ccccc5 --- /dev/null +++ b/users/gkhaykin/programs/agents/mcp.nix @@ -0,0 +1,32 @@ +{ lib, pkgs }: +let + linear.url = "https://mcp.linear.app/mcp"; + notion.url = "https://mcp.notion.com/mcp"; + + mkGrafana = tokenReference: { + type = "stdio"; + command = lib.getExe pkgs.mcp-grafana; + env = { + GRAFANA_URL = "https://togetherai.grafana.net"; + GRAFANA_SERVICE_ACCOUNT_TOKEN = tokenReference; + }; + }; + + cursor = { + inherit linear notion; + grafana = mkGrafana "\${env:GRAFANA_SERVICE_ACCOUNT_TOKEN}"; + }; + + claude = { + linear = linear // { + type = "http"; + }; + notion = notion // { + type = "http"; + }; + grafana = mkGrafana "\${GRAFANA_SERVICE_ACCOUNT_TOKEN}"; + }; +in +{ + inherit cursor claude; +} diff --git a/users/shared/programs/agents/skills/linear-ticket-writing/SKILL.md b/users/gkhaykin/programs/agents/skills/linear-ticket-writing/SKILL.md similarity index 100% rename from users/shared/programs/agents/skills/linear-ticket-writing/SKILL.md rename to users/gkhaykin/programs/agents/skills/linear-ticket-writing/SKILL.md diff --git a/users/shared/programs/agents/skills/slack-pr-announcement/SKILL.md b/users/gkhaykin/programs/agents/skills/slack-pr-announcement/SKILL.md similarity index 100% rename from users/shared/programs/agents/skills/slack-pr-announcement/SKILL.md rename to users/gkhaykin/programs/agents/skills/slack-pr-announcement/SKILL.md diff --git a/users/shared/programs/agents/skills/weekly-cycle-update/SKILL.md b/users/gkhaykin/programs/agents/skills/weekly-cycle-update/SKILL.md similarity index 100% rename from users/shared/programs/agents/skills/weekly-cycle-update/SKILL.md rename to users/gkhaykin/programs/agents/skills/weekly-cycle-update/SKILL.md diff --git a/users/gkhaykin/programs/claude.nix b/users/gkhaykin/programs/claude.nix new file mode 100644 index 0000000..838c6f9 --- /dev/null +++ b/users/gkhaykin/programs/claude.nix @@ -0,0 +1,37 @@ +{ lib, pkgs, ... }: +let + mcpServers = (import ./agents/mcp.nix { inherit lib pkgs; }).claude; + + skillSources = (import ../../shared/programs/agents/skills.nix { inherit lib; }) ./agents/skills; + skillFiles = lib.mapAttrs' ( + skillName: source: + lib.nameValuePair ".claude/skills/${skillName}" { + inherit source; + recursive = true; + } + ) skillSources; + + removeMcpServer = serverName: '' + if printf '%s\n' "$registered_mcp_servers" | grep -q ${lib.escapeShellArg "^${serverName}:"}; then + $DRY_RUN_CMD claude mcp remove ${lib.escapeShellArg serverName} --scope user >/dev/null + fi + ''; + + addMcpServer = serverName: serverConfiguration: '' + $DRY_RUN_CMD claude mcp add-json --scope user ${lib.escapeShellArg serverName} ${lib.escapeShellArg (builtins.toJSON serverConfiguration)} >/dev/null + ''; +in +{ + home.file = skillFiles; + + # Claude stores MCP servers alongside mutable state in ~/.claude.json, so use its CLI to preserve unrelated settings. + home.activation.claudeMcpServers = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + if command -v claude >/dev/null 2>&1; then + # Listing performs health checks, so cache the result before reconciling each server. + registered_mcp_servers="$(claude mcp list 2>/dev/null)" + ${lib.concatMapStringsSep "\n" removeMcpServer (builtins.attrNames mcpServers)} + ${lib.concatStringsSep "\n" (lib.mapAttrsToList addMcpServer mcpServers)} + unset registered_mcp_servers + fi + ''; +} diff --git a/users/gkhaykin/programs/cursor.nix b/users/gkhaykin/programs/cursor.nix new file mode 100644 index 0000000..e8764ca --- /dev/null +++ b/users/gkhaykin/programs/cursor.nix @@ -0,0 +1,19 @@ +{ lib, pkgs, ... }: +let + mcpServers = (import ./agents/mcp.nix { inherit lib pkgs; }).cursor; + + skillSources = (import ../../shared/programs/agents/skills.nix { inherit lib; }) ./agents/skills; + skillFiles = lib.mapAttrs' ( + skillName: source: + lib.nameValuePair ".cursor/skills/${skillName}" { + inherit source; + recursive = true; + } + ) skillSources; +in +{ + home.file = skillFiles // { + # Cursor keeps global MCP configuration in a dedicated file, so Home Manager can own it directly. + ".cursor/mcp.json".text = builtins.toJSON { inherit mcpServers; }; + }; +} diff --git a/users/gkhaykin/programs/default.nix b/users/gkhaykin/programs/default.nix new file mode 100644 index 0000000..c0e9b67 --- /dev/null +++ b/users/gkhaykin/programs/default.nix @@ -0,0 +1,9 @@ +{ ... }: +{ + imports = [ + ./claude.nix + ./cursor.nix + ./finicky.nix + ./vscode + ]; +} diff --git a/users/shared/programs/agents/default.nix b/users/shared/programs/agents/default.nix deleted file mode 100644 index 55632a1..0000000 --- a/users/shared/programs/agents/default.nix +++ /dev/null @@ -1,15 +0,0 @@ -{ ... }: -let - skillsDirectories = { - source = ./skills; - recursive = true; - }; -in -{ - imports = [ ./rules.nix ]; - - home.file = { - ".claude/skills" = skillsDirectories; - ".cursor/skills" = skillsDirectories; - }; -} diff --git a/users/shared/programs/agents/mcp-servers.nix b/users/shared/programs/agents/mcp-servers.nix deleted file mode 100644 index ebec8d2..0000000 --- a/users/shared/programs/agents/mcp-servers.nix +++ /dev/null @@ -1,103 +0,0 @@ -{ lib }: -let - mcpServerDefinitions = { - linear = { - clients = [ - "claude" - "cursor" - ]; - url = "https://mcp.linear.app/mcp"; - }; - - github = { - clients = [ - "claude" - "cursor" - ]; - url = "https://api.githubcopilot.com/mcp/"; - headers.Authorization = { - valuePrefix = "Bearer "; - environmentVariable = "GITHUB_PERSONAL_ACCESS_TOKEN"; - }; - }; - - notion = { - clients = [ - "claude" - "cursor" - ]; - url = "https://mcp.notion.com/mcp"; - }; - - grafana = { - clients = [ "cursor" ]; - url = "https://mcp.grafana.com/mcp"; - headers.X-Grafana-URL.literalValue = "https://togetherai.grafana.net"; - }; - }; - - /** - Renders a literal header or an environment-variable reference using the - syntax expected by the target client. - - # Type - - ``` - renderHeaderValue :: String -> AttrSet -> String - ``` - */ - renderHeaderValue = - clientName: headerDefinition: - if headerDefinition ? environmentVariable then - (headerDefinition.valuePrefix or "") - + ( - if clientName == "cursor" then - "\${env:${headerDefinition.environmentVariable}}" - else - "\${${headerDefinition.environmentVariable}}" - ) - else - headerDefinition.literalValue; - - /** - Translates a shared MCP server definition into a client-specific configuration. - - # Type - - ``` - renderServerConfiguration :: String -> AttrSet -> AttrSet - ``` - */ - renderServerConfiguration = - clientName: serverDefinition: - { - inherit (serverDefinition) url; - } - // lib.optionalAttrs (clientName == "claude") { - type = "http"; - } - // lib.optionalAttrs (serverDefinition ? headers) { - headers = lib.mapAttrs (_: renderHeaderValue clientName) serverDefinition.headers; - }; - - /** - Selects and renders all MCP servers enabled for a client. - - # Type - - ``` - mcpServersForClient :: String -> AttrSet - ``` - */ - mcpServersForClient = - clientName: - lib.mapAttrs (_: renderServerConfiguration clientName) ( - lib.filterAttrs ( - _: serverDefinition: lib.elem clientName serverDefinition.clients - ) mcpServerDefinitions - ); -in -{ - claude = mcpServersForClient "claude"; - cursor = mcpServersForClient "cursor"; -} diff --git a/users/shared/programs/agents/rules.nix b/users/shared/programs/agents/rules.nix index c8dbf2a..e96ab37 100644 --- a/users/shared/programs/agents/rules.nix +++ b/users/shared/programs/agents/rules.nix @@ -1,5 +1,6 @@ -{ lib, pkgs, ... }: +{ lib, pkgs }: let + # NOTE: # Absence of `filePatterns` in a rule means it always loads when the agent is active. rules = { code-principles = { @@ -133,23 +134,8 @@ let mkCursorRule = name: rule: pkgs.writeText "${name}.mdc" (mkCursorMarkdownFrontmatter rule + readRuleContent name); - /** - Creates `home.file` entries for every rule using a tool-specific generator. - - # Type - - ``` - mkHomeRuleFiles :: String -> String -> (String -> AttrSet -> Path) -> AttrSet - ``` - */ - mkHomeRuleFiles = - directory: extension: mkRule: - lib.mapAttrs' ( - name: rule: lib.nameValuePair "${directory}/${name}.${extension}" { source = mkRule name rule; } - ) rules; in { - home.file = - mkHomeRuleFiles ".claude/rules" "md" mkClaudeRule - // mkHomeRuleFiles ".cursor/rules" "mdc" mkCursorRule; + claude = lib.mapAttrs mkClaudeRule rules; + cursor = lib.mapAttrs mkCursorRule rules; } diff --git a/users/shared/programs/agents/skills.nix b/users/shared/programs/agents/skills.nix new file mode 100644 index 0000000..812c8bd --- /dev/null +++ b/users/shared/programs/agents/skills.nix @@ -0,0 +1,18 @@ +{ lib }: +/** + Returns each immediate skill directory keyed by its name. + Non-directory entries are ignored. + + # Type + + ``` + Path -> AttrSet + ``` +*/ +skillsDirectory: +let + skillNames = lib.attrNames ( + lib.filterAttrs (_: entryType: entryType == "directory") (builtins.readDir skillsDirectory) + ); +in +lib.genAttrs skillNames (skillName: skillsDirectory + "/${skillName}") diff --git a/users/shared/programs/claude.nix b/users/shared/programs/claude.nix index 93fce7e..eea17c4 100644 --- a/users/shared/programs/claude.nix +++ b/users/shared/programs/claude.nix @@ -1,29 +1,20 @@ # Claude Code global configuration -{ lib, ... }: +{ lib, pkgs, ... }: let - mcpServers = (import ./agents/mcp-servers.nix { inherit lib; }).claude; + ruleSources = (import ./agents/rules.nix { inherit lib pkgs; }).claude; + ruleFiles = lib.mapAttrs' ( + ruleName: source: lib.nameValuePair ".claude/rules/${ruleName}.md" { inherit source; } + ) ruleSources; - /** - Generates an activation-script fragment that replaces a managed MCP server - with its declared configuration. - - # Type - - ``` - reconcileMcpServer :: String -> AttrSet -> String - ``` - */ - reconcileMcpServer = serverName: serverConfiguration: '' - if claude mcp list 2>/dev/null | grep -q ${lib.escapeShellArg "^${serverName}:"}; then - $DRY_RUN_CMD claude mcp remove ${lib.escapeShellArg serverName} --scope user >/dev/null || true - fi - $DRY_RUN_CMD claude mcp add-json --scope user ${lib.escapeShellArg serverName} ${lib.escapeShellArg (builtins.toJSON serverConfiguration)} >/dev/null || true - ''; + skillSources = (import ./agents/skills.nix { inherit lib; }) ./agents/skills; + skillFiles = lib.mapAttrs' ( + skillName: source: + lib.nameValuePair ".claude/skills/${skillName}" { + inherit source; + recursive = true; + } + ) skillSources; in { - home.activation.claudeMcpServers = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - if command -v claude >/dev/null 2>&1; then - ${lib.concatStringsSep "\n" (lib.mapAttrsToList reconcileMcpServer mcpServers)} - fi - ''; + home.file = ruleFiles // skillFiles; } diff --git a/users/shared/programs/cursor/agents.nix b/users/shared/programs/cursor/agents.nix new file mode 100644 index 0000000..0d491de --- /dev/null +++ b/users/shared/programs/cursor/agents.nix @@ -0,0 +1,19 @@ +{ lib, pkgs, ... }: +let + ruleSources = (import ../agents/rules.nix { inherit lib pkgs; }).cursor; + ruleFiles = lib.mapAttrs' ( + ruleName: source: lib.nameValuePair ".cursor/rules/${ruleName}.mdc" { inherit source; } + ) ruleSources; + + skillSources = (import ../agents/skills.nix { inherit lib; }) ../agents/skills; + skillFiles = lib.mapAttrs' ( + skillName: source: + lib.nameValuePair ".cursor/skills/${skillName}" { + inherit source; + recursive = true; + } + ) skillSources; +in +{ + home.file = ruleFiles // skillFiles; +} diff --git a/users/shared/programs/cursor.nix b/users/shared/programs/cursor/default.nix similarity index 88% rename from users/shared/programs/cursor.nix rename to users/shared/programs/cursor/default.nix index fd3bcc6..2b49c4a 100644 --- a/users/shared/programs/cursor.nix +++ b/users/shared/programs/cursor/default.nix @@ -1,18 +1,22 @@ # Cursor editor configuration -{ config, lib, ... }: +{ + config, + lib, + ... +}: let - managedCursorExtensions = (import ./vscode/extensions.nix) ++ [ + managedCursorExtensions = (import ../vscode/extensions.nix) ++ [ "anthropic.claude-code" "anysphere.cursorpyright" "anysphere.remote-containers" "anysphere.remote-ssh" "hediet.vscode-drawio" ]; - mcpServers = (import ./agents/mcp-servers.nix { inherit lib; }).cursor; in { + imports = [ ./agents.nix ]; + home.file = { - ".cursor/mcp.json".text = builtins.toJSON { inherit mcpServers; }; "Library/Application Support/Cursor/User/keybindings.json" = { force = true; source = config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/.config/dotfiles/users/shared/programs/cursor/keybindings.json"; diff --git a/users/shared/programs/default.nix b/users/shared/programs/default.nix index 7bce31f..f8df2c4 100644 --- a/users/shared/programs/default.nix +++ b/users/shared/programs/default.nix @@ -1,24 +1,29 @@ { ... }: { imports = [ - ./agents - ./iterm2 + # Editors and agents + ./claude.nix + ./cursor ./vscode - ./zsh - ./asdf + # Shell and terminal ./atuin.nix - ./awscli.nix - ./claude.nix - ./cursor.nix ./direnv.nix + ./iterm2 + ./tmux.nix + ./zoxide.nix + ./zsh + + # Development + ./asdf ./gh.nix ./git.nix - ./krew.nix - ./kubie.nix ./nh.nix ./ssh.nix - ./tmux.nix - ./zoxide.nix + + # Cloud and Kubernetes + ./awscli.nix + ./krew.nix + ./kubie.nix ]; } From 8b0574ce7553cdbcd1bc627e4ec03b75d36c795d Mon Sep 17 00:00:00 2001 From: Gleb Khaykin Date: Sun, 16 Aug 2026 21:03:28 +0200 Subject: [PATCH 2/2] feat(finicky): route Together GitHub links to work profile --- users/gkhaykin/programs/finicky.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/users/gkhaykin/programs/finicky.ts b/users/gkhaykin/programs/finicky.ts index a26a9f1..13d2162 100644 --- a/users/gkhaykin/programs/finicky.ts +++ b/users/gkhaykin/programs/finicky.ts @@ -5,13 +5,15 @@ export default { defaultBrowser: "Google Chrome", handlers: [ { - match: finicky.matchHostnames([ - "together.ai", - /\.together\.ai$/, - "together-ai.okta.com", - /\.awsapps\.com$/, - /^oidc\..+\.amazonaws\.com$/, - ]), + match: [ + "together.ai*", + "*.together.ai*", + "together-ai.okta.com*", + "*.awsapps.com*", + "oidc.*.amazonaws.com*", + "github.com/togethercomputer*", + "github.com/orgs/togethercomputer*", + ], browser: { name: "Google Chrome", profile: "Together",