9router/cli
Tri Dung Nguyen 5327a7dc30
fix(autostart): work on nvm + npm 9/10, actually register with launchctl (fixes #1082) (#1104)
* fix(autostart): resolve cli.js path locally, register with launchctl, verify state

The current `cli/src/cli/tray/autostart.js` is silently broken on every nvm
install (and likely Volta / asdf / Homebrew / user-prefix npm) and on every
npm version >= 9. Enabling auto-start from the tray menu writes a launchd
plist that references a non-existent script; the menu then reports
"✓ Auto-start Enabled" because the existence check only verifies the file is
on disk. On the next OS boot launchd fails with MODULE_NOT_FOUND. None of
this surfaces to the user.

This rewrite addresses the four problems reported in #1082:

1. `npm bin -g` was removed in npm 9. autostart.js called it as a primary
   resolution mechanism and silently fell back to a hardcoded
   `/usr/local/lib/node_modules/9router/cli.js` path on failure. Replaced
   with a `getCliJsPath()` helper that tries (in order): the explicit
   `cliPath` argument, `process.argv[1]` when it's our own cli.js, and a
   path computed relative to autostart.js's own location (since this file
   always lives at `<pkg>/src/cli/tray/autostart.js`, cli.js is three
   levels up regardless of install layout). Returns null on no match.

2. The `/usr/local/...` fallback was outright wrong for nvm/Volta/asdf
   installs. Dropped entirely. If no candidate resolves, return false
   instead of writing a plist pointing at a missing script.

3. `enableMacOS()` never called `launchctl load -w`, only `unload`. The
   plist was therefore inert until the next user login, with no signal to
   the user that anything was wrong. Now it unloads (defensive, in case of
   re-enable) and then loads, so the agent is active in the current session.

4. `isAutoStartEnabled()` on macOS only checked file existence — so the
   tray menu reported "✓ Enabled" even when launchd had the agent in a
   failed state or hadn't loaded it. Now also runs `launchctl list
   ${APP_LABEL}` and only returns true if launchd recognizes the label.

Additional changes for robustness:

- The macOS plist now invokes node + cli.js directly with absolute paths
  instead of wrapping in `zsh -l -c "..."`. The shell-wrapper approach
  depended on the user's login shell sourcing nvm/PATH correctly, which is
  fragile (nvm.sh sourcing varies between users; some setups don't add
  node to PATH from a non-interactive login shell).
- `EnvironmentVariables.PATH` in the plist now explicitly includes node's
  bin directory plus the standard system paths, so child processes spawned
  by cli.js (e.g. the runtime `npm install` calls) can still resolve `npm`
  even under launchd's minimal default env.
- Windows and Linux paths were calling `npm bin -g` with the same fallback
  problem; both now use `getCliJsPath()` consistently. The Windows VBS
  branch is simplified (always run node+cli.js, drop the `9router.cmd`
  lookup that depended on the npm prefix path).
- Removed the unused `getStartCommand()` helper that was never imported.

Fixes #1082

* fix(autostart): skip launchctl unload/load when current process is the agent

When the running 9router cli.js was itself spawned by the autostart launchd
agent (after a reboot when autostart was previously enabled), clicking the
tray menu's "Disable Auto-start" item would unload the agent — and that
unload sends SIGTERM to the running process, killing the click handler and
the tray icon before the menu could flip its label.

Add a small `isAgentSelfMacOS()` probe (parses `launchctl list ${label}` and
compares the PID against `process.pid`). When we're the agent itself,
disable skips the unload (the plist file removal is enough to prevent the
agent from starting on the next login) and enable skips the load (the plist
is already loaded under our own PID, the on-disk update is what matters for
next boot). External callers — e.g. enable from a manually-launched 9router
or from a script — still get the full unload/load behavior they need.

Without this, the tray UX after a reboot was: click Disable -> tray icon
silently disappears, no menu label change. Now: click Disable -> label
flips to "Enable Auto-start", tray stays, plist removed; click Enable again
-> label flips back, plist re-created.
2026-05-14 09:57:05 +07:00
..
hooks fix(tray): switch macOS/Linux tray to systray2 fork (#1080) 2026-05-13 15:30:33 +07:00
scripts Fix build bug 2026-05-14 00:28:08 +07:00
src/cli fix(autostart): work on nvm + npm 9/10, actually register with launchctl (fixes #1082) (#1104) 2026-05-14 09:57:05 +07:00
.gitignore TUI Source 2026-05-12 20:26:08 +07:00
.npmignore TUI Source 2026-05-12 20:26:08 +07:00
cli.js TUI Source 2026-05-12 20:26:08 +07:00
LICENSE TUI Source 2026-05-12 20:26:08 +07:00
package.json Fix build bug 2026-05-14 00:28:08 +07:00
README.md chore: clean Docker tags + clearer pulls badge 2026-05-13 22:34:11 +07:00

9Router - FREE AI Router & Token Saver

Never stop coding. Save 20-40% tokens with RTK + auto-fallback to FREE & cheap AI models.

Connect All AI Code Tools (Claude Code, Cursor, Antigravity, Copilot, Codex, Gemini, OpenCode, Cline, OpenClaw...) to 40+ AI Providers & 100+ Models.

npm Downloads Docker Pulls GHCR License

decolua%2F9router | Trendshift

🌐 Website📖 Full Docs


🤔 Why 9Router?

Stop wasting money, tokens and hitting limits:

  • Subscription quota expires unused every month
  • Rate limits stop you mid-coding
  • Tool outputs (git diff, grep, ls...) burn tokens fast
  • Expensive APIs ($20-50/month per provider)

9Router solves this:

  • RTK Token Saver - Auto-compress tool_result, save 20-40% tokens
  • Maximize subscriptions - Track quota, use every bit before reset
  • Auto fallback - Subscription → Cheap → Free, zero downtime
  • Multi-account - Round-robin between accounts per provider
  • Universal - Works with any OpenAI/Claude-compatible CLI

Quick Start

Option 1 — npm (recommended for desktop):

npm install -g 9router
9router

# Or run directly with npx
npx 9router

Option 2 — Docker (server/VPS):

docker run -d --name 9router -p 20128:20128 \
  -v "$HOME/.9router:/app/data" -e DATA_DIR=/app/data \
  decolua/9router:latest

Published images: Docker HubGHCR (multi-platform amd64/arm64).

🎉 Dashboard opens at http://localhost:20128

2. Connect a FREE provider (no signup needed):

Dashboard → Providers → Connect Kiro AI (free Claude unlimited) or OpenCode Free (no auth) → Done!

3. Use in your CLI tool:

Claude Code/Codex/OpenClaw/Cursor/Cline Settings:
  Endpoint: http://localhost:20128/v1
  API Key:  [copy from dashboard]
  Model:    kr/claude-sonnet-4.5

That's it! Start coding with FREE AI models.


🚀 CLI Options

9router                    # Start with default settings
9router --port 8080        # Custom port
9router --no-browser       # Don't open browser
9router --skip-update      # Skip auto-update check
9router --help             # Show all options

Dashboard: http://localhost:20128/dashboard


🛠️ Supported CLI Tools

Claude-Code • OpenClaw • Codex • OpenCode • Cursor • Antigravity • Cline • Continue • Droid • Roo • Copilot • Kilo Code • Gemini CLI • Qwen Code • iFlow • Crush • Crusher • Aider

Any tool supporting OpenAI/Claude-compatible API works.


💾 Data Location

  • macOS/Linux: ~/.9router/db/data.sqlite
  • Windows: %APPDATA%/9router/db/data.sqlite
  • Docker: /app/data/db/data.sqlite (mount $HOME/.9router to persist)

📚 Documentation

Full docs, advanced setup, video tutorials & development guide:


🙏 Acknowledgments

📄 License

MIT License - see LICENSE for details.