dotfiles: add beets, sync-music script, firstmate, ngrok, openssh, gh/tmux/treehouse/opencode/pi brews

This commit is contained in:
Lap Tran
2026-08-08 21:20:27 -04:00
parent 6eee2509a4
commit 4a0bff0331
9 changed files with 326 additions and 36 deletions
+5 -3
View File
@@ -3,9 +3,11 @@
Deliberate decisions in this repo - do NOT silently revert them: 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. - `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. - 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, and opencode still share the same `home/AGENTS.md` for multi-agent work. - 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.
- Shell aliases: `cc` (Claude skip-permissions), `co` (Codex full-auto), `gb` (Grok --yolo). They are intentional high-agency shortcuts. - 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 ## Maintaining this file
+8
View File
@@ -5,6 +5,9 @@
nix.enable = false; nix.enable = false;
programs.zsh.enable = true; 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.config.allowUnfree = true;
nixpkgs.hostPlatform = "aarch64-darwin"; # use x86_64-darwin for Intel CPU nixpkgs.hostPlatform = "aarch64-darwin"; # use x86_64-darwin for Intel CPU
@@ -40,7 +43,12 @@
onActivation.autoUpdate = true; onActivation.autoUpdate = true;
onActivation.extraFlags = [ "--force" ]; onActivation.extraFlags = [ "--force" ];
brews = [ brews = [
"gh"
"herdr" "herdr"
"opencode"
"pi-coding-agent"
"tmux"
"treehouse"
]; ];
casks = [ casks = [
"wezterm" "wezterm"
+2 -1
View File
@@ -22,7 +22,8 @@
nix-homebrew.darwinModules.nix-homebrew nix-homebrew.darwinModules.nix-homebrew
home-manager.darwinModules.home-manager home-manager.darwinModules.home-manager
{ {
home-manager.useGlobalPkgs = true; home-manager.backupFileExtension = "backup";
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true; home-manager.useUserPackages = true;
home-manager.users.laptran = import ./home.nix; home-manager.users.laptran = import ./home.nix;
} }
+127 -22
View File
@@ -1,7 +1,9 @@
{ config, pkgs, ... }: { config, pkgs, lib, ... }:
let let
dotfiles = "${config.home.homeDirectory}/.dotfiles"; home = config.home.homeDirectory;
dotfiles = "${home}/.dotfiles";
firstmateHome = "${home}/Documents/firstmate";
in in
{ {
@@ -16,10 +18,12 @@ in
lazygit lazygit
neovim neovim
nerd-fonts.hack 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; fonts.fontconfig.enable = true;
home.sessionVariables.EDITOR = "nvim"; home.sessionVariables.EDITOR = "nvim";
programs.zsh = { programs.zsh = {
enable = true; enable = true;
autosuggestion.enable = true; # ghost text from history autosuggestion.enable = true; # ghost text from history
@@ -38,6 +42,14 @@ in
cc = "claude --dangerously-skip-permissions"; cc = "claude --dangerously-skip-permissions";
co = "codex --full-auto"; co = "codex --full-auto";
gb = "grok --yolo"; 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 <TOK>`)
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. # Edit-in-place: real file stays in the repo; live path is an out-of-store symlink.
home.file.".config/wezterm".source = # force = true: replace a pre-existing regular file once; source of truth is home/.
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/wezterm"; home.file.".config/wezterm" = {
home.file.".config/nvim".source = source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/wezterm";
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/nvim"; force = true;
home.file.".config/herdr".source = };
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/herdr"; 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) # Claude Code settings (also read by Grok for permissions/compat)
home.file.".claude/settings.json".source = home.file.".claude/settings.json" = {
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.claude/settings.json"; source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.claude/settings.json";
force = true;
};
# Shared agent policy - one file, many harnesses # Shared agent policy - one file, many harnesses
home.file.".claude/CLAUDE.md".source = home.file.".claude/CLAUDE.md" = {
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md"; source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md";
home.file.".codex/AGENTS.md".source = force = true;
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md"; };
home.file.".config/opencode/AGENTS.md".source = home.file.".codex/AGENTS.md" = {
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md"; source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md";
home.file.".grok/AGENTS.md".source = force = true;
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md"; };
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 # Grok Build native config
home.file.".grok/config.toml".source = home.file.".grok/config.toml" = {
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.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"
'';
} }
+15
View File
@@ -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
+1
View File
@@ -1,3 +1,4 @@
onboarding = false
[keys] [keys]
prefix = "ctrl+b" prefix = "ctrl+b"
focus_pane_left = "prefix+h" focus_pane_left = "prefix+h"
+27 -10
View File
@@ -1,11 +1,5 @@
# Grok Build user config - managed via home-manager (edit here, then rebuild if needed). # Grok Build user config - managed via home-manager (edit here, then rebuild if needed).
# Adapted from Claude Code setup in kunchenguid/dotfiles + local preferences. # Source of truth: home/.grok/config.toml in dotfiles.
#
# 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)
[marketplace] [marketplace]
official_marketplace_auto_installed = true official_marketplace_auto_installed = true
@@ -15,14 +9,37 @@ name = "xAI Official"
git = "https://github.com/xai-org/plugin-marketplace.git" git = "https://github.com/xai-org/plugin-marketplace.git"
[models] [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] [ui]
theme = "rosepine" theme = "rosepine"
max_thoughts_width = 120 max_thoughts_width = 120
fork_secondary_model = "grok-build" 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" permission_mode = "always-approve"
yolo = false yolo = false
compact_mode = false compact_mode = false
[cli]
installer = "internal"
+102
View File
@@ -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" </dev/null 2>&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
+39
View File
@@ -1,5 +1,44 @@
#!/usr/bin/env bash #!/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 set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
ln -sfn "$DIR" ~/.dotfiles 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 exec sudo darwin-rebuild switch --flake ~/.dotfiles#lt-mbp