fix(docs+menubar): platform-aware fallback messages and macOS-only guard#1075
Open
MHoroszowski wants to merge 3 commits intodanielmiessler:mainfrom
Open
fix(docs+menubar): platform-aware fallback messages and macOS-only guard#1075MHoroszowski wants to merge 3 commits intodanielmiessler:mainfrom
MHoroszowski wants to merge 3 commits intodanielmiessler:mainfrom
Conversation
playAudio() hardcoded /usr/bin/afplay, which is macOS-only. On Linux, every TTS notification fails with ENOENT and the voice server appears to work but produces no audio (the failure is swallowed by the fire-and-forget curl pattern used at the call sites). Extract player resolution into getAudioPlayer(): - darwin → afplay (unchanged) - linux + ffplay → ffplay -nodisp -autoexit -volume 0..100 - linux + mpg123 → mpg123 -f 0..32768 (PCM scale) - neither → throw with an actionable install hint ffplay is preferred because ffmpeg is widely preinstalled; mpg123 is the lightweight fallback. Both route through PulseAudio, so this works on native Linux and on Windows via WSL2 + WSLg out of the box. Verified on Ubuntu 24.04 / WSL2 (Windows 11): TTS audio plays through WSLg PulseAudio to Windows speakers with no additional configuration. Addresses the audio-playback half of danielmiessler#855. Complementary to danielmiessler#1030, which covers the desktop-notification half (osascript → notify-send) without overlap.
… port check Extends the getAudioPlayer() pattern from danielmiessler#1061 to cover three more macOS- only assumptions that fail silently or visibly on Linux and WSL2: 1. sendNotification() hardcoded /usr/bin/osascript with an AppleScript `display notification` call. On Linux this is ENOENT; on WSL2 it is also ENOENT and the user loses every desktop banner. 2. The hardcoded ~/Library/Logs/pai-voice-server.log path appears in six different scripts (install/uninstall/start/stop/status and the menubar BitBar indicator). On Linux it writes into a non-standard location inside $HOME that XDG-aware tools never discover. 3. status.sh and stop.sh/uninstall.sh gate all port-check logic on `lsof`, which is not installed by default on many Linux and container images. When lsof is absent the "is port 8888 in use?" check silently returns no, even when pai-voice is actively listening. Changes: - server.ts: add three helpers next to getAudioPlayer(). * isWSL() — single source of truth for WSL1/WSL2 detection via /proc/version, short-circuits on non-Linux. * getLogPath() — darwin unchanged; linux/wsl uses ${XDG_DATA_HOME:-$HOME/.local/share}/pai/logs/… * getNotificationCmd() — mirrors getAudioPlayer() shape. darwin → /usr/bin/osascript (literal AppleScript preserved byte-identically so macOS behavior is unchanged). wsl2 → wsl-notify-send if present, else powershell.exe with BurntToast (if the module is importable) or a bare [Windows.UI.Notifications.ToastNotificationManager] one-liner as the final fallback. linux → /usr/bin/notify-send. Call site inside sendNotification() routes through the new helper. The darwin branch produces argv identical to the pre-refactor literal, so macOS is an obvious-by-inspection no-op. - lib/platform.sh (new): shared shell helpers sourced by every script. * pai_is_wsl — matches isWSL() in TS (single detector). * pai_log_path — matches getLogPath() in TS. * pai_port_pids PORT — cascades lsof → ss → netstat, printing one PID per line; returns 1 if nothing listens. POSIX-leaning bash, side-effect free on source, no mkdir, no exit. - install.sh, uninstall.sh, status.sh, start.sh, stop.sh, menubar/pai-voice.5s.sh: source lib/platform.sh, replace literal LOG_PATH assignments with "$(pai_log_path)", and swap lsof-only port checks for pai_port_pids so the scripts work when lsof is unavailable. Darwin launchctl logic is untouched; only the log-path string and the port-check call site change on the macOS flow. Verified on Ubuntu 24.04 / WSL2: - bun bundles server.ts cleanly. - bash -n passes on every modified script. - pai_is_wsl returns 0 inside WSL2 and stays false on pure Linux (no /proc/version microsoft match). - pai_log_path resolves to /home/$USER/.local/share/pai/logs/pai-voice-server.log. - pai_port_pids 8888 returns the live PID via lsof, ss (lsof masked), and netstat (lsof + ss masked) — all three branches confirmed against a running pai-voice.service. - getNotificationCmd() on WSL2 selects the powershell.exe branch when wsl-notify-send is absent; powershell.exe returns exit 0 and fires a toast via BurntToast/WinRT. Darwin paths are preserved byte-identically and were not exercised on hardware (author runs PAI on WSL2 only). Please review the darwin branches carefully — they are intentionally line-for-line equal to the pre-refactor literals. Stacked on top of danielmiessler#1061 (cross-platform audio playback). Should be merged after danielmiessler#1061, or rebased onto main if danielmiessler#1061 lands first.
Four small cross-platform cosmetic fixes that close out the PAI audit: - VoiceServer/install.sh and status.sh told Linux/WSL users the voice server "will use macOS 'say'" as a fallback when no ElevenLabs API key was configured. On non-Darwin hosts `say` does not exist and the server has no built-in TTS fallback. Both scripts now branch on pai_is_darwin (from lib/platform.sh, added in the cross-platform helpers PR) and print an honest message on Linux/WSL. The Darwin strings are preserved byte-identical. - VoiceServer/menubar/install-menubar.sh now has an early platform guard. SwiftBar and BitBar are macOS-only; on Linux/WSL the script prints an explanatory message pointing at getNotificationCmd() in server.ts and exits 0 so CI and cross-platform bootstrap flows can invoke it unconditionally. The guard is a single `uname -s` check; on Darwin the script continues through to the unchanged SwiftBar/BitBar detection logic byte-identically. - Packs/Utilities/INSTALL.md's troubleshooting section for the AudioEditor sub-skill only listed `brew install ffmpeg`. Added collapsible <details> blocks with macOS and Linux/WSL install commands. Other entries (wrangler/npm, bun/curl, fabric) are already cross-platform and were left alone. Stacked on top of the cross-platform helpers PR (adds lib/platform.sh with pai_is_darwin) and the cross-platform audio PR. Rebase onto main after those land. Scope note: Packs/Research/INSTALL.md also has two `brew install fabric` references but the plan scoped PR 4 to Utilities/INSTALL.md specifically. Left for a follow-up.
MHoroszowski
added a commit
to MHoroszowski/Personal_AI_Infrastructure
that referenced
this pull request
Apr 15, 2026
VoiceServer's install/start/stop/status/uninstall scripts previously
assumed macOS/launchctl exclusively. Linux and WSL2 users had no
supported path to run the voice server as a supervised service. This
adds a systemd --user branch to each script, selected at runtime via
the pai_is_darwin helper from lib/platform.sh. The Darwin launchctl
flow is preserved byte-identical.
### install.sh
- New systemd_unit_* configuration constants alongside the existing
PLIST_PATH.
- "Existing installation" check branches on pai_is_darwin. On
Linux/WSL it probes systemctl --user list-unit-files and the on-
disk unit file, prompts the user with the same y/n reinstall UX
as the macOS path, and on decline exits 0 without touching the
live unit.
- Linux/WSL prechecks that systemctl is present and that
systemctl --user list-units --no-pager is reachable. On WSL2 it
prints the /etc/wsl.conf [boot] systemd=true hint if the user
session is unavailable.
- New unit generator writes ~/.config/systemd/user/pai-voice.service
templated on the reference unit that ships with PAI on WSL2:
[Unit] Description / After=default.target
[Service] Type=simple WorkingDirectory=${SCRIPT_DIR}
ExecStart=${BUN_BIN} run server.ts
Restart=on-failure RestartSec=3
StandardOutput/StandardError=append:${LOG_PATH}
Environment=HOME/PATH
[Install] WantedBy=default.target
LOG_PATH comes from pai_log_path (XDG on Linux, Library/Logs on
macOS). BUN_BIN is resolved with command -v bun at install time.
HOME and PATH are set explicitly so the child process can find
the user's ~/.env and runtime helpers like mpg123. The unit
passes systemd-analyze verify with no warnings.
- daemon-reload + enable --now starts and persists the service.
Failure prints the systemctl/journalctl commands to diagnose.
- Post-install summary branches by service manager ("launchd" vs
"systemd --user") and the stale "macOS Say (fallback)" voice
string is now Darwin-only, matching the honest message PR danielmiessler#1075
introduced elsewhere.
### start.sh
- Darwin path preserved byte-identical (LaunchAgent existence check,
launchctl list, launchctl load, START_RC capture).
- Linux/WSL branch checks $SYSTEMD_UNIT_PATH for existence,
systemctl --user is-active --quiet for the already-running fast
path, and systemctl --user start otherwise. The "already running"
hint on Linux points at `systemctl --user restart` instead of the
macOS-only ./restart.sh.
### stop.sh
- Darwin path preserved byte-identical.
- Linux/WSL branch: systemctl --user is-active --quiet → stop → ok.
The existing pai_port_pids-based port-8888 cleanup at the tail of
the script stays common to both platforms (unchanged from danielmiessler#1072).
### status.sh
- Service Status block branches on pai_is_darwin. Linux/WSL reads
systemctl --user is-active + MainPID, falling back to
list-unit-files for the installed-but-inactive state, and prints
"not installed" if neither.
- The Voice Configuration block (Darwin "Using macOS 'say'" vs
Linux "No TTS fallback") from PR danielmiessler#1075 is untouched.
### uninstall.sh
- Confirmation banner branches so Linux/WSL says "Remove the
systemd --user unit" instead of "Remove the LaunchAgent".
- Stop-and-remove block branches on pai_is_darwin. Linux/WSL path
stops the unit, disables it, removes the file, and daemon-reloads.
- The optional log-file cleanup and post-uninstall notes are
platform-agnostic and unchanged.
### Reference unit
Templated on the working pai-voice.service unit that ships with PAI
on WSL2 (Description, After, Type, Restart, StandardOutput/Error
format, WantedBy). Differences from the reference:
- WorkingDirectory uses ${SCRIPT_DIR} instead of %h/.claude/VoiceServer
so fork checkouts at any path work correctly.
- ExecStart uses $(command -v bun) instead of %h/.bun/bin/bun so
non-default bun install locations work.
- Log path uses pai_log_path (XDG on Linux) instead of
%h/.claude/VoiceServer/voice-server.log so logs land in the
XDG-compliant location introduced by danielmiessler#1072.
- Explicit Environment=HOME and Environment=PATH so the service can
locate ~/.env and runtime helpers (mpg123, ffplay, etc.)
regardless of how the systemd --user session was launched.
### Idempotency
On a machine with an already-active pai-voice.service unit, re-
running install.sh prompts for reinstall (y/n). Declining exits
cleanly without touching the live unit file or the running process
(verified on the author's machine: PID and unit mtime unchanged
across a full install.sh run with 'n' answer). Accepting will stop,
disable, rewrite, daemon-reload, enable, and start, matching the
exact UX of the macOS reinstall path.
### Verification
On Ubuntu 24.04 / WSL2 with systemd --user:
- bash -n passes on all five modified scripts.
- systemd-analyze verify on the generated unit: clean exit.
- Generated plist content (Darwin branch) is byte-identical to the
pre-refactor heredoc — confirmed by running the unaltered heredoc
body with identical stubs and diffing. The Darwin branch only
gains an enclosing `if pai_is_darwin; then ... fi` wrapper; the
heredoc body lines are at their original columns so the plist
written to disk is byte-identical.
- install.sh with an existing unit file + 'n' answer: hits the
"Installation cancelled" path, exit 0, live unit mtime and the
running service PID both unchanged.
- start.sh against a live running unit: hits "already running",
exit 0, live PID unchanged.
- start.sh with a missing unit (fake HOME): hits "Service not
installed", exit 1, no systemctl invocation.
- status.sh against a live running unit: reports "OK Service is
active (PID: ...)" with the real MainPID.
- status.sh with a bogus unit name: hits "Service is not installed".
- stop.sh sliced with a bogus unit name: hits "not running" branch,
no side effects.
- uninstall.sh with 'n' answer: prints Linux-specific confirmation
banner, "Uninstall cancelled", live state untouched.
Darwin path not exercised on hardware (author has no Mac). The
launchctl/plist code paths are wrapped in `if pai_is_darwin; then`
with the pre-refactor content preserved verbatim, and the generated
plist is confirmed byte-identical. Darwin reviewers please spot-
check the wrapped launchctl flow for any regressions.
Stacked on danielmiessler#1061, danielmiessler#1072, danielmiessler#1075. Rebase onto main after those land.
11 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four small cross-platform cosmetic fixes that close out the PAI audit
for Linux/WSL users:
VoiceServer/install.sh and VoiceServer/status.sh told
Linux/WSL users the voice server "will use macOS 'say'" as a
fallback when no ElevenLabs API key was configured. On non-Darwin
hosts
saydoesn't exist and the server has no built-in TTSfallback. Both scripts now branch on
pai_is_darwin(fromlib/platform.sh) and print an honest message on Linux/WSL. TheDarwin strings are preserved byte-identical.
VoiceServer/menubar/install-menubar.sh now has an early
platform guard. SwiftBar and BitBar are macOS-only; on Linux/WSL
the script prints an explanation pointing at
getNotificationCmd()in
server.tsand exits 0 cleanly so CI and cross-platformbootstrap flows can invoke it unconditionally. The guard is a
single
uname -scheck; on Darwin it is a no-op and executioncontinues through the unchanged SwiftBar/BitBar detection logic
byte-identically.
Packs/Utilities/INSTALL.md's troubleshooting section for the
AudioEditor sub-skill only listed
brew install ffmpeg. Addedcollapsible
<details>blocks with macOS and Linux/WSL installcommands. Other entries (wrangler/npm, bun/curl, fabric) are
already cross-platform and were left alone.
Stacking
This PR stacks on:
fix(VoiceServer): cross-platform audio playback in playAudio()fix(VoiceServer): cross-platform desktop notifications, log path, and port checkPR #1072 introduces
lib/platform.shwithpai_is_darwin, which PR4 reuses. The diff against
mainwill show both parents' contentuntil they merge. Rebase onto
mainafter they land.Test plan
bash -non all three modified shell scriptsinstall-menubar.shrun on real Linux host: guard fires,prints explanation, exits 0
install-menubar.shwith mockeduname -s= Darwin: guard isa no-op, script continues through to the unchanged SwiftBar /
BitBar detection branch
status.shVoice Configuration branch, all three env-filestates (missing / placeholder / real key) on Linux — each hits
the non-Darwin branch and prints the truthful message
install.shfallback note branches on Linux — both no-fileand placeholder-key print the truthful message
Packs/Utilities/INSTALL.mdcollapsible blocks arewell-formed (blank line after
<summary>for GitHub'smarkdown parser)
the three literal strings
Voice server will use macOS 'say' command as fallback,! Using macOS 'say' (no API key), and! Using macOS 'say' (no configuration)still print, and thatinstall-menubar.shruns through SwiftBar/BitBar detectionexactly as it did before. Author runs PAI on WSL2 only.
Scope note
Packs/Research/INSTALL.mdalso has twobrew install fabricreferences but the internal plan scoped PR 4 to
Packs/Utilities/INSTALL.mdspecifically. Left for a follow-up.