{ config, pkgs, lib, ... }: let home = config.home.homeDirectory; dotfiles = "${home}/.dotfiles"; firstmateHome = "${home}/Documents/firstmate"; in { home.username = "laptran"; home.homeDirectory = "/Users/laptran"; home.stateVersion = "24.11"; home.packages = with pkgs; [ ripgrep fd fzf jq lazygit neovim nerd-fonts.hack ngrok # reverse TCP tunnel so the phone can SSH in over cellular (unfree) beets # music tagger / library organizer + navidrome sync plugin ]; fonts.fontconfig.enable = true; home.sessionVariables.EDITOR = "nvim"; programs.zsh = { enable = true; autosuggestion.enable = true; # ghost text from history syntaxHighlighting.enable = true; # commands turn green when valid initContent = '' bindkey '^f' autosuggest-accept ''; shellAliases = { ".." = "cd .."; ll = "ls -plart"; add = "git add ."; push = "git push"; pull = "git pull"; m = "git switch main"; # High-agency agent launchers (same idea across tools) cc = "claude --dangerously-skip-permissions"; co = "codex --full-auto"; gb = "grok --yolo"; # firstmate primary session via OpenCode 2 (isolated config; crewmates = Pi) fm = "cd ${firstmateHome} && exec ${home}/.local/bin/oc2"; oc2 = "${home}/.local/bin/oc2"; # Sync ~/Music into the beets library on Unraid (requires NAS mounted). sync-music = "~/.local/bin/sync-music"; # Start the reverse tunnel so you can SSH into this Mac from your phone. # Run once when you go remote: `ngrok-tunnel`. Reads authtoken from # ~/.config/ngrok (set once per machine with `ngrok config add-authtoken `) ngrok-tunnel = "ngrok tcp 22"; }; }; programs.starship = { enable = true; settings = { add_newline = false; format = "$directory$git_branch$git_status$cmd_duration$line_break$character"; character = { success_symbol = "[❯](purple)"; error_symbol = "[❯](red)"; }; cmd_duration.format = "[$duration]($style) "; }; }; # Edit-in-place: real file stays in the repo; live path is an out-of-store symlink. # force = true: replace a pre-existing regular file once; source of truth is home/. home.file.".config/wezterm" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/wezterm"; force = true; }; home.file.".config/nvim" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/nvim"; force = true; }; home.file.".config/herdr" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/herdr"; force = true; }; # Beets music library manager - managed by nixpkgs package, config symlinked below. home.file.".config/beets" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/beets"; force = true; }; # Claude Code settings (also read by Grok for permissions/compat) home.file.".claude/settings.json" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.claude/settings.json"; force = true; }; # Shared agent policy - one file, many harnesses home.file.".claude/CLAUDE.md" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md"; force = true; }; home.file.".codex/AGENTS.md" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md"; force = true; }; home.file.".config/opencode/AGENTS.md" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md"; force = true; }; # OpenCode 2 isolated config - never share ~/.config/opencode with 1.x home.file.".config/opencode2/opencode.json" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/opencode2/opencode.json"; force = true; }; home.file.".config/opencode2/AGENTS.md" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md"; force = true; }; home.file.".local/bin/oc2" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/bin/oc2"; force = true; }; home.file.".grok/AGENTS.md" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md"; force = true; }; # Grok Build native config home.file.".grok/config.toml" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.grok/config.toml"; force = true; }; # Pi agent - source of truth under home/.pi (sessions/auth/npm stay live under ~/.pi) home.file.".pi/agent/settings.json" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/settings.json"; force = true; }; home.file.".pi/agent/models.json" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/models.json"; force = true; }; home.file.".pi/agent/themes" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/themes"; force = true; }; home.file.".pi/agent/extensions/terminal-status-title.js" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/extensions/terminal-status-title.js"; force = true; }; home.file.".pi/agent/extensions/calm" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/extensions/calm"; force = true; }; home.file.".pi/agent/AGENTS.md" = { source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md"; force = true; }; # firstmate is a mutable agent distro (self-update, state/, projects/). Clone once; # never put it in the Nix store. Seed Pi+herdr defaults only when absent. # Do NOT npm install -g here: activation PATH often resolves Nix's npm, which # cannot write into the store (EACCES). Use Homebrew's node for globals: # /opt/homebrew/bin/npm install -g tasks-axi quota-axi no-mistakes gh-axi lavish-axi chrome-devtools-axi home.activation.firstmate = lib.hm.dag.entryAfter [ "writeBoundary" ] '' set -euo pipefail fm="${firstmateHome}" git="${pkgs.git}/bin/git" if [ ! -d "$fm/.git" ]; then mkdir -p "$(dirname "$fm")" $git clone https://github.com/kunchenguid/firstmate.git "$fm" fi mkdir -p "$fm/config" "$fm/data" "$fm/state" "$fm/projects" # Local gitignored operating choices (do not overwrite captain edits) [ -f "$fm/config/backend" ] || printf 'herdr\n' > "$fm/config/backend" [ -f "$fm/config/crew-harness" ] || printf 'pi\n' > "$fm/config/crew-harness" # Crew dispatch profile - source of truth in dotfiles (reproducible across # machines). Symlinked into firstmate config so a rebuild restores routing. # Crewmates run on Pi + opencode-go/mimo-v2.5-pro (see crew-dispatch.json). ln -sfn "${dotfiles}/home/.config/firstmate/crew-dispatch.json" "$fm/config/crew-dispatch.json" # Gitea PR helper for local-only mode: after the first mate (opencode) # reviews a crewmate branch, this pushes it + opens a Gitea PR via tea. ln -sfn "${dotfiles}/home/bin/fm-gitea-pr.sh" "$HOME/.local/bin/fm-gitea-pr.sh" ''; # Authorize the SSH key so the phone can log in through the ngrok tunnel. # (home-manager 26.05 removed programs.ssh.authorizedKeys; manage the file # here so ~/.ssh is 0700 and authorized_keys is 0600. Public key is not a secret.) home.activation.authorizeSSHKey = lib.hm.dag.entryAfter [ "writeBoundary" ] '' set -euo pipefail mkdir -p "$HOME/.ssh" chmod 700 "$HOME/.ssh" printf '%s\n' "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGCZEGVYMDztSryFwoZ6cfpBH3ksP3h0yxZSanlcbrZ0 unraid-omada" > "$HOME/.ssh/authorized_keys" chmod 600 "$HOME/.ssh/authorized_keys" ''; # Wire the sync-music script into ~/.local/bin without touching anything else # that lives there (node, python3.11, hermes, etc.). Source of truth is the # dotfiles repo so a rebuild restores it if it ever disappears. home.activation.syncMusic = lib.hm.dag.entryAfter [ "writeBoundary" ] '' set -euo pipefail ln -sfn "${dotfiles}/home/bin/sync-music" "$HOME/.local/bin/sync-music" ''; # OpenCode 2 is not on Homebrew (beta). Install via Homebrew's npm, never # Nix's npm (EACCES on the store). Skip when already present so a rebuild # does not pull a new beta under a live TUI. home.activation.opencode2 = lib.hm.dag.entryAfter [ "writeBoundary" ] '' set -euo pipefail npm=/opt/homebrew/bin/npm bin=/opt/homebrew/bin/opencode2 if [ -x "$bin" ]; then exit 0 fi if [ ! -x "$npm" ]; then echo "opencode2: /opt/homebrew/bin/npm missing (declare node in homebrew.brews)" >&2 exit 1 fi "$npm" install -g --allow-scripts=@opencode-ai/cli @opencode-ai/cli@beta ''; }