Use a LaunchAgent so npm run dev:server starts when you log in and restarts after reboot. This is the HTTP stack only (no tunnel). For remote MCP or HTTPS tunneling, run npm run dev:server:tunnel from a terminal when needed.
From the Neotoma repo root:
npm run setup:launchd-devAlias: npm run setup:launchd-dev-server (same as setup:launchd-dev).
This will:
- Unload and remove the legacy
com.neotoma.dev-serversagent if it was installed (that label randev:server:tunnel). - Install
~/Library/LaunchAgents/com.neotoma.dev-server.plistwith your repo path. - Create
data/logsif needed (logs go todata/logs/launchd-dev-server.log). - Unload
com.neotoma.dev-serverif it was already loaded, then load the agent so the dev server starts or restarts immediately (same pattern assetup:launchd-issues-sync).
After reboot, the agent runs again automatically (RunAtLoad + KeepAlive).
The agent runs npm run dev:server. It sources .env from the repo root if present.
The API process is started via scripts/run-neotoma-api-node-watch.sh. On macOS, restarts are driven by scripts/run_neotoma_api_chokidar_poll_watch.js (chokidar with polling) by default. That covers two related native-watch failure modes: under some LaunchAgent sessions, Node’s --watch uses fs.watch / FSEvents and can miss saves; in interactive sessions it can also leave a watch supervisor alive after the server child exits, so the HTTP port is no longer bound even though the dev stack still looks alive. On non-macOS hosts, interactive npm run dev:server still uses Node’s native watch (node --watch-path=… --import tsx …) unless polling is explicitly requested.
Opt out of polling on macOS (restore Node --watch): set NEOTOMA_API_WATCH_NATIVE=1 or NEOTOMA_API_WATCH_FORCE_POLL=0 in the repo .env. Force polling on any platform with NEOTOMA_API_WATCH_FORCE_POLL=1. Tune chokidar interval with NEOTOMA_API_WATCH_POLL_INTERVAL_MS (milliseconds, default 750). The plist also sets TSC_WATCHFILE / TSC_WATCHDIRECTORY so any stacked tsc --watch in the same npm tree falls back to polling when needed.
After neotoma cli config (signed or unsigned stdio shims), neotoma-dev and neotoma entries set NEOTOMA_MCP_USE_LOCAL_PORT_FILE=1 and NEOTOMA_MCP_LOCAL_HTTP_PORT_PROFILE so the shims probe .dev-serve/local_http_port_dev / local_http_port_prod the same way as documented for prod. Re-run neotoma cli config --yes (or your transport preset) after upgrading the CLI so mcp.json picks up the env. Optional: keep npm run setup:launchd-cli-sync so global neotoma + dist/ stay aligned with src/ when you are not running only tsx.
Re-install the plist after template changes: npm run setup:launchd-dev.
To reload an already-installed Neotoma agent without re-running the full installer (after local plist edits or to bounce the process): npm run reload:launchd-neotoma from any directory on macOS. That script only touches Neotoma-owned labels under ~/Library/LaunchAgents (see scripts/reload_neotoma_launchagents.sh).
To clear orphan dev/prod API processes that survived a crashed reload (the failure mode that accumulated 50+ stale dev API instances on ports 3145–3179 prior to v0.12.0): bash scripts/reload_neotoma_launchagents.sh --kill-zombies. The flag SIGTERMs four orphan patterns rooted in the current repo before re-loading the LaunchAgents:
node dist/index.jswithPPID=1— legacy zombies adopted by launchd after a crash.scripts/with_branch_ports.js node --import tsx … src/actions.tschains plus their immediatetsxserver child — left over from chokidar reload chains that did not propagateSIGTERMto the API server group.tsx watch … src/actions.tschains rooted in this repo — left behind bynpm test/vitestintegration suites.npm exec tsx … src/actions.tsinvocations from manual debug runs.
The v0.12.0 dev-server watcher now spawns the API as a process-group leader and SIGTERMs the whole group on reload, so the steady-state orphan rate is near zero. --kill-zombies remains the recovery path when an earlier (pre-v0.12.0) install left zombies on disk, or when an integration test forced-killed a parent without cleaning up its server worker.
To fully stop the Neotoma launchd stack (dev/prod/watch-build/issues-sync plus leftover launchd-owned server/watch processes): npm run shutdown:launchd-neotoma.
| Action | Command |
|---|---|
| Load (start) | launchctl load ~/Library/LaunchAgents/com.neotoma.dev-server.plist |
| Unload (stop) | launchctl unload ~/Library/LaunchAgents/com.neotoma.dev-server.plist |
| Status | launchctl list | grep neotoma |
| Logs | tail -f data/logs/launchd-dev-server.log |
launchctl unload ~/Library/LaunchAgents/com.neotoma.dev-server.plistRemove the agent entirely:
rm ~/Library/LaunchAgents/com.neotoma.dev-server.plistFor a separate always-on built API in production mode (build + default prod port + start:server), see docs/developer/launchd_prod_server.md and npm run setup:launchd-prod-server.
npm run setup:launchd-rc-autodeploy installs com.neotoma.rc-autodeploy, which keeps the running prod server current with origin/main unattended. Every 120s it runs scripts/redeploy_rc_from_main.sh, which:
git fetch+ fast-forward-only pull oforigin/maininto the RC checkout, preserving the uncommitted RC version bump (e.g.0.16.0-rc.1) via stash/pop; it refuses to proceed on a non-fast-forward divergence.- Rebuilds
dist(npm run build:server). - Hard-restarts
com.neotoma.prod-server(launchctl kickstart -k) so the process re-imports fresh modules — a soft reload was observed to miss reducer changes.
It is idempotent (no-op when the RC already equals origin/main) and single-flight (atomic mkdir lock). This is mechanical deploy only — it makes no release judgment. Cutting tagged releases remains a separate, gated step (e.g. the Ateles Struthio release agent). Override HEALTH_URL / poll interval via the plist if needed.
- macOS only. LaunchAgents are a macOS feature. On Linux, use a systemd user service or similar.
- Dev environment. This agent does not set
NEOTOMA_ENV=production. Use the prod LaunchAgent or your own wrapper for that.