Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Brewfile
Original file line number Diff line number Diff line change
Expand Up @@ -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/)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The gateway regex now lives in two languages: is_gateway_host in bootstrap.sh and this line. The comment points at the bash version, but nothing makes the two stay in step. If one is widened later (for example to a new hostname), the other drifts silently. A small check in tests/test-bootstrap.sh could catch that. It would assert that the Brewfile contains mac-mini(-\d+)? and bootstrap.sh contains mac-mini(-[0-9]+)?, or extract both and run the same sample hostnames through each.

cask "jellyfin" # Media server (http://localhost:8096)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The cask only installs Jellyfin.app. Nothing starts it at login, so after a reboot the server is down until someone launches it by hand. If it is meant to be always-on like the other gateway services, consider adding it as a Login Item, or adding a LaunchAgents/ plist gated by is_gateway_host like com.evansenter.obsidian-mcp. It could also go in the CLAUDE.md Infrastructure Services table (port 8096). That can be a follow-up if you would rather keep this PR install-only.

end

# Casks - Fonts
cask "font-jetbrains-mono-nerd-font"

Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
53 changes: 53 additions & 0 deletions LaunchAgents/com.evansenter.jellyfin.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.evansenter.jellyfin</string>

<!-- Runs the server binary from the jellyfin cask headless, in place of the
Jellyfin.app menu-bar wrapper (which can't be given an environment). -->
<key>ProgramArguments</key>
<array>
<string>/Applications/Jellyfin.app/Contents/MacOS/jellyfin</string>
<string>--webdir</string>
<string>/Applications/Jellyfin.app/Contents/Resources/jellyfin-web</string>
<string>--ffmpeg</string>
<string>/Applications/Jellyfin.app/Contents/MacOS/ffmpeg</string>
<string>--datadir</string>
<string>__HOME__/Library/Application Support/jellyfin</string>
</array>

<!-- On macOS 27, starting the FSEvents-backed config watcher blocks forever
and the server never logs or binds 8096; polling avoids FSEvents. -->
<key>EnvironmentVariables</key>
<dict>
<key>DOTNET_USE_POLLING_FILE_WATCHER</key>
<string>1</string>
</dict>

<key>RunAtLoad</key>
<true/>

<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>

<key>ThrottleInterval</key>
<integer>30</integer>

<!-- stdout duplicates the rotating logs Jellyfin writes under its datadir/log;
keep only stderr for crashes before logging is up. -->
<key>StandardOutPath</key>
<string>/dev/null</string>

<key>StandardErrorPath</key>
<string>__HOME__/.local/log/jellyfin.err</string>

<!-- Standard, not Background: Background caps CPU/IO, which starves transcoding. -->
<key>ProcessType</key>
<string>Standard</string>
</dict>
</plist>
5 changes: 5 additions & 0 deletions bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions tests/test-bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 '<string>1</string>'
}

test_launch_agent_idempotent_on_unchanged_plist() {
_load_install_launch_agent
local root home stub; root=$(mktemp -d); home=$(mktemp -d); stub=$(mktemp -d)
Expand Down Expand Up @@ -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 ==="
Expand Down
Loading