From 4fb50afc76417cf20ae588a9d4c1d33c5f9c1769 Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Thu, 22 Oct 2026 23:24:46 +0100 Subject: [PATCH 1/3] feat(brew): install the Jellyfin media server on the gateway host Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LBftoc1JCMLow9LDFZMADs --- Brewfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Brewfile b/Brewfile index a896c50..3f447d1 100644 --- a/Brewfile +++ b/Brewfile @@ -70,6 +70,12 @@ cask "steam" cask "transmission" # BitTorrent client cask "whatsapp" +# Gateway-only (mac-mini): same hostname match as is_gateway_host in bootstrap.sh +require "socket" +if Socket.gethostname.split(".").first.match?(/\Amac-mini(-\d+)?\z/) + cask "jellyfin" # Media server (http://localhost:8096) +end + # Casks - Fonts cask "font-jetbrains-mono-nerd-font" From f4db3e6bd0d820224b874906f87321b1d9c38a47 Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Thu, 22 Oct 2026 23:33:54 +0100 Subject: [PATCH 2/3] feat(launchd): run the Jellyfin server headless with a polling file watcher On macOS 27, .NET's FSEvents-backed config watcher blocks forever at startup, so the server never logs or binds 8096. Run the cask's server binary from a gateway-only LaunchAgent with DOTNET_USE_POLLING_FILE_WATCHER=1. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LBftoc1JCMLow9LDFZMADs --- CLAUDE.md | 3 +- LaunchAgents/com.evansenter.jellyfin.plist | 50 ++++++++++++++++++++++ bootstrap.sh | 5 +++ tests/test-bootstrap.sh | 7 +++ 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 LaunchAgents/com.evansenter.jellyfin.plist diff --git a/CLAUDE.md b/CLAUDE.md index d7dcfa9..8b80aac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -116,10 +116,11 @@ Files in `home/` are symlinked to `~` by `bootstrap.sh`. This allows version con | agent-session-analytics | 8081 | `/agent-session-analytics` | `com.evansenter.agent-session-analytics` | | agent-memory-store | 8083 | `/agent-memory-store` | `com.evansenter.agent-memory-store` | | obsidian-mcp | 3010 | `/obsidian-mcp` | `com.evansenter.obsidian-mcp` | +| jellyfin | 8096 | — | `com.evansenter.jellyfin` | LaunchAgent plists live in `~/Library/LaunchAgents/`. The external service repos (event-bus, session-analytics, memory-store) have their own `make install-server`; obsidian-mcp is set up by *this* repo's bootstrap (`claude mcp add` + plist in `LaunchAgents/`, wrapper in `home/.bin/obsidian-mcp-start`, requires Obsidian with the Local REST API plugin running). Reload with `launchctl unload` + `launchctl load`. -Note: `agent-memory-store` is documented infra but is **not** provisioned by bootstrap (no `claude mcp add`, no `AGENT_MEMORY_STORE_URL` in `settings.local.json`) — it's registered out-of-band where used. The repo-managed LaunchAgents are: `com.evansenter.obsidian-mcp` (gateway only), `com.evansenter.sysload` (zellij CPU/RAM widget, if zellij installed), and `com.user.cargo-sweep` (periodic `cargo sweep`, if cargo installed). +Note: `agent-memory-store` is documented infra but is **not** provisioned by bootstrap (no `claude mcp add`, no `AGENT_MEMORY_STORE_URL` in `settings.local.json`) — it's registered out-of-band where used. The repo-managed LaunchAgents are: `com.evansenter.obsidian-mcp` (gateway only), `com.evansenter.jellyfin` (gateway only, runs the server binary from the `jellyfin` cask headless with `DOTNET_USE_POLLING_FILE_WATCHER=1` — on macOS 27 its FSEvents config watcher blocks startup forever; don't also launch Jellyfin.app, which would fight it for port 8096), `com.evansenter.sysload` (zellij CPU/RAM widget, if zellij installed), and `com.user.cargo-sweep` (periodic `cargo sweep`, if cargo installed). **Adding new LaunchAgents:** Plists go in the top-level `LaunchAgents/` directory (NOT `home/Library/LaunchAgents/`). Use `__HOME__` as a placeholder for the user's home directory — `install_launch_agent()` in bootstrap does `sed` substitution at install time. Logs go to `~/.local/log/`. Add an `install_launch_agent` call in `install_launch_agents()`, gated on the binary being present. diff --git a/LaunchAgents/com.evansenter.jellyfin.plist b/LaunchAgents/com.evansenter.jellyfin.plist new file mode 100644 index 0000000..ae0bb3d --- /dev/null +++ b/LaunchAgents/com.evansenter.jellyfin.plist @@ -0,0 +1,50 @@ + + + + + Label + com.evansenter.jellyfin + + + ProgramArguments + + /Applications/Jellyfin.app/Contents/MacOS/jellyfin + --webdir + /Applications/Jellyfin.app/Contents/Resources/jellyfin-web + --ffmpeg + /Applications/Jellyfin.app/Contents/MacOS/ffmpeg + --datadir + __HOME__/Library/Application Support/jellyfin + + + + EnvironmentVariables + + DOTNET_USE_POLLING_FILE_WATCHER + 1 + + + RunAtLoad + + + KeepAlive + + SuccessfulExit + + + + ThrottleInterval + 30 + + StandardOutPath + __HOME__/.local/log/jellyfin.stdout + + StandardErrorPath + __HOME__/.local/log/jellyfin.err + + ProcessType + Background + + diff --git a/bootstrap.sh b/bootstrap.sh index a06c436..750902b 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -393,6 +393,11 @@ install_launch_agents() { install_launch_agent "com.evansenter.obsidian-mcp.plist" fi + # Install Jellyfin server LaunchAgent (gateway host, from the jellyfin cask) + if is_gateway_host && [[ -x /Applications/Jellyfin.app/Contents/MacOS/jellyfin ]]; then + install_launch_agent "com.evansenter.jellyfin.plist" + fi + # Install sysload writer LaunchAgent (only if zellij is installed) # Caches `top` output to ~/.cache/sysload every 10s so the zjstatus # sysload widget can read it without spawning `top` per-tab-per-redraw. diff --git a/tests/test-bootstrap.sh b/tests/test-bootstrap.sh index 061ca40..a11e63a 100755 --- a/tests/test-bootstrap.sh +++ b/tests/test-bootstrap.sh @@ -704,6 +704,12 @@ test_launch_agent_substitutes_home() { return $ok } +test_jellyfin_agent_uses_polling_file_watcher() { + # Without it the server hangs before logging or binding 8096 on macOS 27 + grep -A1 'DOTNET_USE_POLLING_FILE_WATCHER' "$SCRIPT_DIR/../LaunchAgents/com.evansenter.jellyfin.plist" \ + | grep -q '1' +} + test_launch_agent_idempotent_on_unchanged_plist() { _load_install_launch_agent local root home stub; root=$(mktemp -d); home=$(mktemp -d); stub=$(mktemp -d) @@ -1030,6 +1036,7 @@ main() { run_test "sysload-writer tolerates top failure" "test_sysload_writer_tolerates_top_failure" run_test "launch agent substitutes __HOME__ and reloads" "test_launch_agent_substitutes_home" run_test "launch agent idempotent on unchanged plist" "test_launch_agent_idempotent_on_unchanged_plist" + run_test "jellyfin agent uses polling file watcher" "test_jellyfin_agent_uses_polling_file_watcher" echo "" echo "=== INSTALL_AI & INSTALL_TAILSCALE gating ===" From 7ca3214e4eeef5489eb55bbbbec7d8f8ffd3779e Mon Sep 17 00:00:00 2001 From: Evan Senter Date: Thu, 22 Oct 2026 23:37:39 +0100 Subject: [PATCH 3/3] fix(launchd): don't throttle Jellyfin transcoding or duplicate its logs Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01LBftoc1JCMLow9LDFZMADs --- LaunchAgents/com.evansenter.jellyfin.plist | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/LaunchAgents/com.evansenter.jellyfin.plist b/LaunchAgents/com.evansenter.jellyfin.plist index ae0bb3d..9559371 100644 --- a/LaunchAgents/com.evansenter.jellyfin.plist +++ b/LaunchAgents/com.evansenter.jellyfin.plist @@ -38,13 +38,16 @@ ThrottleInterval 30 + StandardOutPath - __HOME__/.local/log/jellyfin.stdout + /dev/null StandardErrorPath __HOME__/.local/log/jellyfin.err + ProcessType - Background + Standard