From 4a0bff0331f3191c02cf0861af847700d8133240 Mon Sep 17 00:00:00 2001 From: Lap Tran Date: Sat, 8 Aug 2026 21:20:27 -0400 Subject: [PATCH] dotfiles: add beets, sync-music script, firstmate, ngrok, openssh, gh/tmux/treehouse/opencode/pi brews --- AGENTS.md | 8 +- configuration.nix | 8 ++ flake.nix | 3 +- home.nix | 149 ++++++++++++++++++++++++++++----- home/.config/beets/config.yaml | 15 ++++ home/.config/herdr/config.toml | 1 + home/.grok/config.toml | 37 +++++--- home/bin/sync-music | 102 ++++++++++++++++++++++ rebuild.sh | 39 +++++++++ 9 files changed, 326 insertions(+), 36 deletions(-) create mode 100644 home/.config/beets/config.yaml create mode 100644 home/bin/sync-music diff --git a/AGENTS.md b/AGENTS.md index 7127cbe..b108b46 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,9 +3,11 @@ Deliberate decisions in this repo - do NOT silently revert them: - `homebrew.onActivation.cleanup = "zap"` in `configuration.nix` is intentional. It forces the good habit of declaring every Homebrew package in the Nix config instead of installing things ad-hoc, which keeps the machine reproducible. Do not soften it to `uninstall` or `none`. Users are warned about its effect; this note is for anyone tempted to change the setting itself. -- Agent configs under `home/` are the source of truth. `home.nix` installs them with `mkOutOfStoreSymlink`, so edits to files in `home/` apply live after the first rebuild that creates the symlink. Prefer editing those paths, not the live `~/.claude` / `~/.grok` copies. -- Grok Build is first-class here: `home/.grok/config.toml` and `home/AGENTS.md` (linked as `~/.grok/AGENTS.md`). Claude, Codex, and opencode still share the same `home/AGENTS.md` for multi-agent work. -- Shell aliases: `cc` (Claude skip-permissions), `co` (Codex full-auto), `gb` (Grok --yolo). They are intentional high-agency shortcuts. +- Agent configs under `home/` are the source of truth. `home.nix` installs them with `mkOutOfStoreSymlink` + `force = true`, so edits to files in `home/` apply live after the first rebuild that creates the symlink. Prefer editing those paths, not the live `~/.claude` / `~/.grok` / `~/.pi` copies. `rebuild.sh` rotates stale `*.backup` files before switch so home-manager never fails with "would be clobbered". +- Grok Build is first-class here: `home/.grok/config.toml` and `home/AGENTS.md` (linked as `~/.grok/AGENTS.md`). Claude, Codex, opencode, and Pi share the same `home/AGENTS.md` for multi-agent work. +- Pi is managed the same way: `home/.pi/agent/{settings,models}.json`, `themes/`, and custom extensions (`terminal-status-title`, `calm`). Do not symlink the whole `~/.pi/agent` tree - sessions, auth, npm cache, and herdr's `extensions/herdr-agent-state.ts` stay live. +- firstmate lives at `~/Documents/firstmate` as a mutable git clone (not in the Nix store). `home.activation.firstmate` clones it if missing and seeds `config/backend=herdr` + `config/crew-harness=pi` only when those files are absent. Launch with shell alias `fm` (`cd` + `pi`). Approve Pi project trust once so `.pi/extensions/*.ts` load. Self-update via `/updatefirstmate`, not rebuild. +- Shell aliases: `cc` (Claude skip-permissions), `co` (Codex full-auto), `gb` (Grok --yolo), `fm` (firstmate via Pi). They are intentional high-agency shortcuts. ## Maintaining this file diff --git a/configuration.nix b/configuration.nix index 55fc53e..d97a97d 100644 --- a/configuration.nix +++ b/configuration.nix @@ -5,6 +5,9 @@ nix.enable = false; programs.zsh.enable = true; + # SSH server (macOS 14.4+ gates `systemsetup -setremotelogin` behind Full Disk Access; nix-darwin loads ssh.plist via launchd directly instead). + services.openssh.enable = true; + nixpkgs.config.allowUnfree = true; nixpkgs.hostPlatform = "aarch64-darwin"; # use x86_64-darwin for Intel CPU @@ -40,7 +43,12 @@ onActivation.autoUpdate = true; onActivation.extraFlags = [ "--force" ]; brews = [ + "gh" "herdr" + "opencode" + "pi-coding-agent" + "tmux" + "treehouse" ]; casks = [ "wezterm" diff --git a/flake.nix b/flake.nix index 4c0e8a3..5961eba 100644 --- a/flake.nix +++ b/flake.nix @@ -22,7 +22,8 @@ nix-homebrew.darwinModules.nix-homebrew home-manager.darwinModules.home-manager { - home-manager.useGlobalPkgs = true; + home-manager.backupFileExtension = "backup"; + home-manager.useGlobalPkgs = true; home-manager.useUserPackages = true; home-manager.users.laptran = import ./home.nix; } diff --git a/home.nix b/home.nix index 2d1eb02..e0493ed 100644 --- a/home.nix +++ b/home.nix @@ -1,7 +1,9 @@ -{ config, pkgs, ... }: +{ config, pkgs, lib, ... }: let - dotfiles = "${config.home.homeDirectory}/.dotfiles"; + home = config.home.homeDirectory; + dotfiles = "${home}/.dotfiles"; + firstmateHome = "${home}/Documents/firstmate"; in { @@ -16,10 +18,12 @@ in 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 @@ -38,6 +42,14 @@ in cc = "claude --dangerously-skip-permissions"; co = "codex --full-auto"; gb = "grok --yolo"; + # firstmate primary session via Pi (approve project trust once per clone) + fm = "cd ${firstmateHome} && exec pi"; + # 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"; }; }; @@ -54,29 +66,122 @@ in }; }; - # Edit-in-place: the real file stays in my repo, the live path just points at it. - home.file.".config/wezterm".source = - config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/wezterm"; - home.file.".config/nvim".source = - config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/nvim"; - home.file.".config/herdr".source = - config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/herdr"; + # 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"; + 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"; - home.file.".codex/AGENTS.md".source = - config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md"; - home.file.".config/opencode/AGENTS.md".source = - config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md"; - home.file.".grok/AGENTS.md".source = - config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md"; + 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; + }; + 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"; + 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" + ''; + + # 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" + ''; } + diff --git a/home/.config/beets/config.yaml b/home/.config/beets/config.yaml new file mode 100644 index 0000000..02d29c7 --- /dev/null +++ b/home/.config/beets/config.yaml @@ -0,0 +1,15 @@ +directory: /Volumes/data/media/music +library: /Volumes/data/media/music/beets/beets_library.db + +import: + move: yes + copy: no + resume: ask + timid: no + quiet: no + +paths: + default: $artist/$album/$track $title + singleton: Non-Album/$artist/$title + comp: Compilations/$album/$track $title + albumtype_soundtrack: Soundtracks/$album/$track $title diff --git a/home/.config/herdr/config.toml b/home/.config/herdr/config.toml index 8fa93ac..5e79fbc 100644 --- a/home/.config/herdr/config.toml +++ b/home/.config/herdr/config.toml @@ -1,3 +1,4 @@ +onboarding = false [keys] prefix = "ctrl+b" focus_pane_left = "prefix+h" diff --git a/home/.grok/config.toml b/home/.grok/config.toml index 19072f0..05421e3 100644 --- a/home/.grok/config.toml +++ b/home/.grok/config.toml @@ -1,11 +1,5 @@ # Grok Build user config - managed via home-manager (edit here, then rebuild if needed). -# Adapted from Claude Code setup in kunchenguid/dotfiles + local preferences. -# -# Mapping notes: -# Claude --dangerously-skip-permissions -> permission_mode = "always-approve" / --yolo -# Claude theme dark-ansi + rose-pine stack -> theme = "rosepine" -# Claude statusLine (model | ctx %) -> no direct equivalent; TUI shows this natively -# Shared AGENTS.md -> ~/.grok/AGENTS.md (via home.nix) +# Source of truth: home/.grok/config.toml in dotfiles. [marketplace] official_marketplace_auto_installed = true @@ -15,14 +9,37 @@ name = "xAI Official" git = "https://github.com/xai-org/plugin-marketplace.git" [models] -default = "grok-build" +default = "ornith" + +[model.ornith] +model = "/models/ornith-1.0-35b-Q6_K.gguf" +base_url = "http://10.37.0.165:30081/v1" +name = "Ornith 1.0 35B (Talos)" +description = "Local Ornith-1.0-35B Q6_K via llama.cpp on Talos worker 1 (RTX 4070 Super)" +api_backend = "chat_completions" +temperature = 0.7 +top_p = 0.95 +context_window = 132768 +max_completion_tokens = 16384 + +[model.genesis] +model = "/models/Hermes3.6-35B-A3B-Uncensored-Genesis-V6-APEX.gguf" +base_url = "http://10.37.0.219:30082/v1" +name = "Genesis Hermes V6 (Talos W2)" +description = "Local Qwen3.6-35B-A3B Genesis Hermes V6 APEX + vision via llama.cpp on Talos worker 2 (RTX 3080)" +api_backend = "chat_completions" +temperature = 0.7 +top_p = 0.95 +context_window = 131072 +max_completion_tokens = 16384 [ui] theme = "rosepine" max_thoughts_width = 120 fork_secondary_model = "grok-build" -# High-agency default, same idea as `claude --dangerously-skip-permissions` / `codex --full-auto`. -# Toggle per session with Ctrl+O or /always-approve. Shell alias `gb` also launches with --yolo. permission_mode = "always-approve" yolo = false compact_mode = false + +[cli] +installer = "internal" diff --git a/home/bin/sync-music b/home/bin/sync-music new file mode 100644 index 0000000..7facbfc --- /dev/null +++ b/home/bin/sync-music @@ -0,0 +1,102 @@ +#!/bin/bash +# sync-music — manually sync ~/Music into the beets library on the Unraid NAS. +# +# sync-music # shell alias -> ~/.local/bin/sync-music +# +# How it works: +# 1. Ensures the NAS SMB share is mounted at /Volumes/data (uses your keychain +# credentials; falls back to a clear error if it can't mount). +# 2. Imports every audio file in ~/Music into beets via +# `beet import --noautotag --move --quiet`, which moves each file into the +# library and removes it from ~/Music. Files already in the library are +# skipped by beets (left in place). Safe to re-run any time. +# +# Why a manual script (not the old daemon)? The daemon kept dying because +# launchd/WezTerm spawn a minimal PATH that lacks /opt/homebrew/bin (where +# `beet` lives). Running this script yourself in your own shell avoids that. +# +# Configure your server below. + +NAS_HOST="unraid.local" +NAS_SHARE="data" +NAS_USER="backup" +MOUNT_POINT="/Volumes/data" +LIBRARY_DIR="$MOUNT_POINT/media/music" + +WATCH_DIR="$HOME/Music" +LOG_FILE="$HOME/Library/Logs/beet-watch.log" + +AUDIO_EXT="flac|mp3|m4a|aac|wav|alac|ogg|wma|opus|aiff|aif" + +log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; } + +# --- locate beet — works whether it comes from nix profile or Homebrew --- +BEET="$(command -v beet 2>/dev/null || true)" +if [ ! -x "$BEET" ]; then + echo "ERROR: beet not found." >&2 + exit 1 +fi + +# --- 1. ensure the NAS is mounted --- +if [ ! -d "$MOUNT_POINT" ]; then + log "NAS not mounted; mounting smb://$NAS_USER@$NAS_HOST/$NAS_SHARE ..." + open "smb://$NAS_USER@$NAS_HOST/$NAS_SHARE" 2>/dev/null || true + for _ in $(seq 1 30); do + [ -d "$MOUNT_POINT" ] && break + sleep 1 + done +fi +if [ ! -d "$MOUNT_POINT" ]; then + echo "ERROR: $MOUNT_POINT is not mounted." >&2 + echo " Mount it, then re-run:" >&2 + echo " open smb://$NAS_USER@$NAS_HOST/$NAS_SHARE" >&2 + exit 1 +fi + +# --- 2. import every (finished) audio file in ~/Music --- +log "sync-music: scanning $WATCH_DIR -> $LIBRARY_DIR" + +shopt -s nullglob nocaseglob +files=() +for f in "$WATCH_DIR"/*; do + [ -f "$f" ] || continue + [[ "${f##*.}" =~ ^($AUDIO_EXT)$ ]] && files+=("$f") +done +shopt -u nocaseglob nullglob + +total=${#files[@]} +log "Found $total audio file(s)." + +ok=0; skipped=0; failed=0 +for f in "${files[@]}"; do + # skip files still being written (size must be stable for 1s) + s1=$(stat -f%z "$f" 2>/dev/null || echo 0) + sleep 1 + s2=$(stat -f%z "$f" 2>/dev/null || echo 0) + if [ "$s1" != "$s2" ]; then + log " still writing, skipping: $(basename "$f")" + skipped=$((skipped + 1)) + continue + fi + + bn=$(basename "$f") + log " importing: $bn" + # stdin from /dev/null so beets never blocks waiting for a prompt + out=$("$BEET" import --noautotag --move --quiet "$f" &1) + rc=$? + if [ $rc -eq 0 ]; then + if [ -f "$f" ]; then + log " already in library (skipped, left in place): $bn" + else + log " moved into library: $bn" + fi + ok=$((ok + 1)) + else + log " FAILED (rc=$rc): $bn :: $(printf '%s' "$out" | head -c 200)" + failed=$((failed + 1)) + fi +done + +log "sync-music complete: $total considered, ok=$ok, still-writing-skipped=$skipped, failed=$failed" +[ "$failed" -gt 0 ] && log " some imports failed; fix and re-run." +exit 0 diff --git a/rebuild.sh b/rebuild.sh index 9a4a578..6e4348c 100755 --- a/rebuild.sh +++ b/rebuild.sh @@ -1,5 +1,44 @@ #!/usr/bin/env bash +# Rebuild nix-darwin + home-manager. Rotates stale home-manager *.backup files +# so a second switch never fails with "would be clobbered". set -euo pipefail DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" ln -sfn "$DIR" ~/.dotfiles + +stamp="$(date +%Y%m%d-%H%M%S)" +# Paths home-manager manages via home.file (regular files become out-of-store symlinks). +managed=( + "$HOME/.grok/config.toml" + "$HOME/.grok/AGENTS.md" + "$HOME/.claude/settings.json" + "$HOME/.claude/CLAUDE.md" + "$HOME/.codex/AGENTS.md" + "$HOME/.config/opencode/AGENTS.md" + "$HOME/.pi/agent/settings.json" + "$HOME/.pi/agent/models.json" + "$HOME/.pi/agent/AGENTS.md" + "$HOME/.pi/agent/extensions/terminal-status-title.js" +) +for path in "${managed[@]}"; do + bak="${path}.backup" + if [ -e "$bak" ] || [ -L "$bak" ]; then + mv "$bak" "${bak}.${stamp}" + echo "rotated stale backup: ${bak} -> ${bak}.${stamp}" + fi +done +# Directory targets use the same backup suffix on collision. +for path in \ + "$HOME/.config/wezterm" \ + "$HOME/.config/nvim" \ + "$HOME/.config/herdr" \ + "$HOME/.pi/agent/themes" \ + "$HOME/.pi/agent/extensions/calm" +do + bak="${path}.backup" + if [ -e "$bak" ] || [ -L "$bak" ]; then + mv "$bak" "${bak}.${stamp}" + echo "rotated stale backup: ${bak} -> ${bak}.${stamp}" + fi +done + exec sudo darwin-rebuild switch --flake ~/.dotfiles#lt-mbp