starmux 0.15.0 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.16.1
4
+
5
+ ### Discovery and documentation
6
+
7
+ - Refine the npm package description to highlight the self-hosted remote MCP gateway, supported AI clients, Desktop Commander tools, and Linux / NVIDIA Jetson access.
8
+ - Update npm search keywords for MCP gateways, remote computer access, and compatible AI coding clients.
9
+ - Add task-focused use-case guides, including the tested WorkBuddy-to-Linux / NVIDIA Jetson connection and its verified `streamable-http` configuration.
10
+ - No runtime behavior or dependency changes in this release.
11
+
12
+ ## 0.16.0
13
+
14
+ - Keep instance and port-lease identities in the private, durable `~/.config/starmux/control` directory instead of temporary storage.
15
+ - Use short hashed Unix socket paths and stop the owned worker if a control record or socket disappears, preserving a diagnostic reason.
16
+ - Allow a supervisor to finish cleanup after its own metadata is deleted, while refusing to remove a replaced control directory or another run's records.
17
+ - Add regression tests for deleting each instance/port owner record and Unix socket during a live run.
18
+ - Stop instances before changing to this control layout.
19
+
3
20
  ## 0.15.0
4
21
 
5
22
  ### Instance lifecycle
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  <div align="center">
2
2
 
3
- # Starmux MCP Gateway
3
+ # Starmux Self-hosted MCP Gateway
4
4
 
5
5
  ### Give cloud AI clients secure access to your local machine.
6
6
 
7
7
  **Self-hosted MCP Gateway · Built-in File, Shell & Git Tools · macOS Menu Bar App · MIT**
8
8
 
9
- [Why Starmux?](#why-starmux) · [Quick Start](#quick-start) · [Connecting AI Clients](#connecting-ai-clients) · [macOS App](#native-macos-app) · [Documentation](docs/README.md)
9
+ [Why Starmux?](#why-starmux) · [Quick Start](#quick-start) · [Use Cases](#common-use-cases) · [Securing & Client Guides](#securing-remote-access--client-guides) · [macOS App](#native-macos-app) · [Documentation](docs/README.md)
10
10
 
11
11
  </div>
12
12
 
@@ -14,18 +14,18 @@
14
14
 
15
15
  ## Why Starmux?
16
16
 
17
- Cloud AI assistants (like **ChatGPT Web**, **Claude**, **Codex**, or **WorkBuddy**) are powerful, but they are trapped in the cloud—isolated from your local codebases, terminal, and development environment.
17
+ AI assistants and coding agents (like **ChatGPT Web**, **Claude App / Code**, **Antigravity**, **Codex**, **Cursor**, **WorkBuddy**, **DSH-Harness**, **Kimi Code**, **Minimax Code**, **ZCode**, or **Doubao**) are powerful, but cloud clients are isolated in the browser and many desktop agents lack native Linux / ARM64 apps.
18
18
 
19
- **Starmux** (`Star` topology + `Multiplexer`) is a self-hosted MCP gateway that connects remote AI clients to the real tools on your own computers over Direct HTTPS or a Secure Tunnel:
19
+ **Starmux** (`Star` topology + `Multiplexer`) is a self-hosted MCP gateway that connects any AI client to the real tools on your own computers:
20
20
 
21
21
  <div align="center">
22
22
  <img src="https://raw.githubusercontent.com/cookyecat/starmux/main/docs/assets/starmux-architecture.svg" width="100%" alt="Starmux architecture: Many AI clients connecting to self-hosted computers via Direct HTTPS or Secure MCP Tunnel" />
23
23
  </div>
24
24
 
25
- - **Batteries-Included Workspace Tools:** Powered by a built-in [Desktop Commander](https://github.com/wonderwhy-er/DesktopCommanderMCP) engine (installed automatically via npm). Out of the box, your AI can read/write files, search code, run Shell commands, and manage Git.
26
- - **Code on Any Host (macOS, Linux, Windows & Edge Devices):** Many AI clients (like **ChatGPT Desktop** or **WorkBuddy**) have no native Linux or ARM64 client. Because Starmux runs anywhere **Node.js 22.12+** runs—including **macOS**, **Linux (`x64` / `arm64`)**, **NVIDIA Jetson**, headless servers, and **Windows**—you can use your favorite AI client on your main computer or browser to develop directly on any target machine.
27
- - **Two Clear Permission Modes:** Starts in global **Read-Only** mode (`starmux up`): AI can read files your OS account and the upstream server permit, while writes and arbitrary execution are blocked. Explicitly opt into **Coding Mode** (`starmux up --code`) to use native tools with your OS account permissions.
28
- - **Your Network, Your Keys:** No vendor-hosted relay required. Protect your gateway with Bearer **API Keys**, browser-based **OAuth**, or your existing **Tunnel / Reverse Proxy** access controls. Revoke access instantly at any time.
25
+ - **Give ChatGPT & Cloud AI Local "Computer Use":** Turn browser and chat AI into a local coding agent. Out of the box, your AI can read/edit local files, search code, run Shell commands, and manage Git via the built-in [Desktop Commander](https://github.com/wonderwhy-er/DesktopCommanderMCP) engine.
26
+ - **Code on Any Host (macOS, Linux, NVIDIA Jetson & Windows):** Desktop AI clients like **WorkBuddy** or **ChatGPT** have no native Linux or ARM64 app. Run Starmux on your target Linux server, **NVIDIA Jetson**, or workstation, and drive it remotely from your laptop or browser.
27
+ - **Programmable MCP Multiplexer & Middleware:** Aggregate built-in workspace tools and your own local `stdio` MCP servers behind a single Streamable HTTP `/mcp` endpoint—complete with unified authentication, tool allowlists, and live activity logs.
28
+ - **100% Self-Hosted — Your Network, Your Keys:** Totally self-hosted. No vendor-hosted relay required. Connect over Localhost, SSH, Direct HTTPS, or your own Tunnel, and lock down access with Bearer API Keys or OAuth 2.0 whenever you need.
29
29
 
30
30
  ---
31
31
 
@@ -39,132 +39,164 @@ Requires **Node.js 22.12+**.
39
39
  npm install -g starmux
40
40
  ```
41
41
 
42
- **Upgrading an existing installation?** Stop each running instance with the current CLI (`starmux down`, or `starmux down -i NAME`) before installing the new version, then run `starmux up` again. See the [upgrade instructions](https://github.com/cookyecat/starmux/blob/main/docs/INSTALL.md#upgrade) and [release notes](https://github.com/cookyecat/starmux/blob/main/CHANGELOG.md).
42
+ *(Upgrading? Run `starmux down` before installing the new version—see the [Upgrade Guide](docs/INSTALL.md#upgrade).)*
43
43
 
44
44
  ### 2. Start in your project directory
45
45
 
46
46
  ```bash
47
47
  cd /path/to/your/project
48
48
 
49
- # Option A: Global read-only mode (default — inspect files & code)
49
+ # Option 1: Read-only mode (default — safely inspect files & code)
50
50
  starmux up
51
51
 
52
- # Option B: Full coding mode (edit files, run Shell & Git)
52
+ # or, Option 2: Coding mode (edit files, run Shell & Git)
53
53
  starmux up --code
54
54
  ```
55
55
 
56
- Starmux initializes itself and runs in the background on port `18893` (coding mode asks for confirmation the first time):
56
+ Starmux starts in the background on `http://127.0.0.1:18893/mcp` with **Authentication: `None`** by default so you can connect immediately:
57
57
 
58
- Switch back from Coding mode with
59
-
60
- ```
61
- starmux up --read
58
+ ```text
59
+ ● Starmux is running (instance: default)
60
+ Instance <instance UUID>
61
+ Listen http://127.0.0.1:18893/mcp (this computer only)
62
+ Project /path/to/your/project
63
+ Mode Read-only (global · OS account) (run 'starmux up --code' to enable edits & shell)
64
+ Auth None (no token required)
62
65
  ```
63
66
 
64
- Add `-i NAME` for a named instance. Both `up` and `status` show the mode-switch command.
67
+ *(Switch modes anytime with `starmux up --code` and `starmux up --read`.)*
68
+
69
+ ### 3. Expose for Remote / Cloud AI *(Skip if your AI client is on this same computer)*
65
70
 
66
- `Project` provides context for relative file paths in Read-only mode; it does not restrict access to that directory. Read-only applies across the computer wherever the OS account and upstream server permit reading. Coding forwards native operations with the account's permissions. Neither mode uses Starmux directory grants or path exclusions.
71
+ Because Starmux only listens on `127.0.0.1:18893`, forward traffic to it if your AI client is in the cloud (e.g., **ChatGPT Web**) or on another computer:
67
72
 
68
- In Read-only mode, `read_multiple_files` supports up to 20 files and 4 MiB of combined file sizes per batch. Missing files retain Desktop Commander's per-file errors alongside successful contents. Oversized batches return `BATCH_READ_COUNT_LIMIT` or `BATCH_READ_SIZE_LIMIT` with guidance to split the request.
73
+ - **Option 1: Reachable IP (Public or LAN) — Direct Proxy with [Caddy](https://caddyserver.com):**
74
+ Run in your terminal (replace `mcp.example.com` with your public domain, or `http://YOUR_LAN_IP` on a local network):
75
+ ```bash
76
+ caddy run --adapter caddyfile --config - << 'EOF'
77
+ mcp.example.com {
78
+ handle /mcp {
79
+ reverse_proxy 127.0.0.1:18893 {
80
+ header_up Host 127.0.0.1:18893
81
+ }
82
+ }
83
+ }
84
+ EOF
85
+ ```
69
86
 
70
- ### 3. Add Starmux to your AI client
87
+ - **Option 2: Behind NAT / Firewall — Use a Tunnel or SSH:**
88
+ - **[OpenAI Secure MCP Tunnel](https://developers.openai.com/api/docs/guides/secure-mcp-tunnels) (for ChatGPT Web / Codex):**
89
+ ```bash
90
+ tunnel-client run \
91
+ --control-plane.tunnel-id "tunnel_YOUR_TUNNEL_ID" \
92
+ --control-plane.api-key "env:MAC_MCP_TUNNEL_KEY" \
93
+ --mcp.server-url "http://127.0.0.1:18893/mcp" \
94
+ --health.listen-addr "127.0.0.1:18891" \
95
+ --log.level info
96
+ ```
97
+ *(Behind a proxy? Append `--control-plane.http-proxy "http://127.0.0.1:PROXY_PORT"`.)*
98
+ - **or, Cloudflare Quick Tunnel:** `cloudflared tunnel --url http://127.0.0.1:18893 --http-host-header 127.0.0.1:18893`
99
+ - **or, SSH Forwarding (Laptop $\rightarrow$ Linux / Jetson):** `ssh -N -L 127.0.0.1:18893:127.0.0.1:18893 user@remote-host` *(add `-p PORT` if SSH uses a non-default port; change the first `18893` to `18894` if port `18893` is already used on your laptop)*
71
100
 
72
- Need a Bearer API key for remote access? Generate one first (optional for local use):
101
+ *(See [Installation & Networking](docs/INSTALL.md#4-remote-network-forwarding--ingress-engineering) for Tailscale, ngrok, and custom TLS ports.)*
73
102
 
74
- ```bash
75
- starmux auth key "My AI Client"
76
- ```
103
+ ### 4. Connect your AI client
104
+
105
+ With default settings (`Auth: None`), connect your AI client using any of the options below:
77
106
 
78
- Depending on your AI client, configure the MCP server using either **Form UI** or **JSON**:
107
+ #### Option 1 — Ask your AI coding agent to add it for you *(Zero manual config)*
79
108
 
80
- #### Option A — AI clients with a Form UI (e.g., ChatGPT Web, Claude)
109
+ For agentic coding clients (**Claude Code**, **Antigravity**, **Codex**, **Cursor**, **WorkBuddy**, **DSH-Harness**, **Kimi Code**, **Minimax Code**, **ZCode**), simply send this prompt in the AI chat (replacing the URL with your LAN IP, domain, or `http://127.0.0.1:18893/mcp`):
81
110
 
82
- Copy the fields directly from `starmux up` (or open `starmux dashboard` in your browser):
111
+ ```text
112
+ Create this MCP Server connector:
113
+ "starmux": {
114
+ "type": "streamable-http",
115
+ "url": "http://192.168.50.114/mcp"
116
+ }
117
+ ```
118
+
119
+ Your AI agent will automatically update its own MCP configuration and connect.
120
+
121
+ #### or, Option 2 — AI clients with a Form UI (e.g., ChatGPT Web, Claude, Doubao)
83
122
 
84
123
  | MCP Client Field | Value to Fill In |
85
124
  | :--- | :--- |
86
- | **Transport** | `Streamable HTTP` |
87
- | **URL (Local)** | `http://127.0.0.1:18893/mcp` *(when the AI client is on this same computer)* |
88
- | **URL (Remote)** | `https://<your-domain-or-tunnel>/mcp` *(forwarded to `127.0.0.1:18893` via Caddy or Tunnel)* |
89
- | **Authentication** | *Optional:* `None` *(default)*, `Bearer API key` (`Authorization: Bearer <YOUR_API_KEY>`), or `OAuth` |
90
-
91
- #### Option B — AI clients with JSON config (e.g., WorkBuddy, Cursor, Windsurf)
125
+ | **Transport** | `Streamable HTTP` *(or select your **Tunnel** in ChatGPT Web)* |
126
+ | **URL** | `https://mcp.example.com/mcp` *(Remote HTTPS / Tunnel)* — or `http://127.0.0.1:18893/mcp` *(same computer)* |
127
+ | **Authentication** | `None` *(default)* |
92
128
 
93
- Paste one of the blocks below into your client's `mcp.json` / `mcpServers` settings.
129
+ #### or, Option 3 — Manually paste into `mcp.json`
94
130
 
95
- **Without API key (default for local `127.0.0.1` or when your tunnel handles login — no `headers` needed):**
131
+ If you prefer editing your client's `mcp.json` / `mcpServers` settings manually:
96
132
 
97
- ```json
98
- {
99
- "mcpServers": {
100
- "starmux": {
101
- "type": "streamable-http",
102
- "url": "http://127.0.0.1:18893/mcp"
133
+ - **Remote / LAN connection (via Direct Proxy or Tunnel — do NOT use `127.0.0.1` across separate machines):**
134
+ ```json
135
+ {
136
+ "mcpServers": {
137
+ "starmux": {
138
+ "type": "streamable-http",
139
+ "url": "https://mcp.example.com/mcp"
140
+ }
103
141
  }
104
142
  }
105
- }
106
- ```
107
-
108
- **With Bearer API key (`headers` is optional — include it after running `starmux auth key`):**
109
-
110
- ```json
111
- {
112
- "mcpServers": {
113
- "starmux": {
114
- "type": "streamable-http",
115
- "url": "https://<your-domain-or-tunnel>/mcp",
116
- "headers": {
117
- "Authorization": "Bearer smx_YOUR_GENERATED_API_KEY"
143
+ ```
144
+
145
+ - **or, Same-computer connection (or when using local `ssh -L` port forwarding):**
146
+ ```json
147
+ {
148
+ "mcpServers": {
149
+ "starmux": {
150
+ "type": "streamable-http",
151
+ "url": "http://127.0.0.1:18893/mcp"
118
152
  }
119
153
  }
120
154
  }
121
- }
122
- ```
155
+ ```
123
156
 
124
157
  ---
125
158
 
126
- ## Connecting AI Clients
127
-
128
- By design, Starmux only listens on `127.0.0.1:18893/mcp` (loopback) and never exposes raw ports directly to external networks:
159
+ ## Common Use Cases
129
160
 
130
- - **Local AI Client (Same Computer):** Connects directly to `http://127.0.0.1:18893/mcp` with zero network configuration.
131
- - **Remote / Cloud AI Client:** Connects through an **HTTPS Reverse Proxy** or a **Tunnel Client** running on your machine, which receives external HTTPS traffic and forwards it locally to `127.0.0.1:18893/mcp`:
161
+ - [How can ChatGPT access my local computer?](https://github.com/cookyecat/starmux/blob/main/docs/use-cases/chatgpt-access-local-computer.md)
162
+ - [How do I self-host a remote MCP gateway?](https://github.com/cookyecat/starmux/blob/main/docs/use-cases/self-hosted-remote-mcp-gateway.md)
163
+ - [How can I use Desktop Commander remotely?](https://github.com/cookyecat/starmux/blob/main/docs/use-cases/remote-desktop-commander.md)
164
+ - [Can ChatGPT access a Linux server or NVIDIA Jetson?](https://github.com/cookyecat/starmux/blob/main/docs/use-cases/chatgpt-linux-jetson.md)
165
+ - [How can WorkBuddy operate Linux or NVIDIA Jetson remotely?](https://github.com/cookyecat/starmux/blob/main/docs/use-cases/workbuddy-linux-jetson.md)
166
+ - [How do I connect an MCP server behind NAT — HTTPS or Tunnel?](https://github.com/cookyecat/starmux/blob/main/docs/use-cases/mcp-behind-nat-https-vs-tunnel.md)
167
+ - [How can I secure a remote MCP endpoint with OAuth?](https://github.com/cookyecat/starmux/blob/main/docs/use-cases/self-hosted-mcp-oauth.md)
132
168
 
133
- ```text
134
- Remote AI Client
135
- ──(HTTPS)──▶ HTTPS Reverse Proxy (Caddy) or Tunnel Client
136
- ──(Loopback)──▶ Starmux (127.0.0.1:18893/mcp)
137
- ──▶ Local Files, Shell & Git
138
- ```
139
-
140
- ### 1. Choose a Network Forwarder (for Remote / Cloud Access)
169
+ ---
141
170
 
142
- | Network Method | Best For | Recommended Software & Official Docs | Starmux Guide |
143
- | :--- | :--- | :--- | :--- |
144
- | **Direct HTTPS (Reverse Proxy)** | Hosts with a public IPv4 / IPv6 address and a domain name | [Caddy](https://caddyserver.com/docs/quick-starts/reverse-proxy) *(auto HTTPS)* · [Nginx](https://nginx.org/en/docs/http/ngx_http_proxy_module.html) | [Caddy HTTPS Setup](docs/REMOTE_SETUP.md#2-https--bearer-api-key) |
145
- | **Secure Tunnel** | Hosts behind NAT, home Wi-Fi, or office firewalls *(no inbound port needed)* | [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) · [Tailscale Funnel / Serve](https://tailscale.com/kb/1223/funnel) · [OpenAI Secure MCP Tunnel](https://developers.openai.com/api/docs/guides/secure-mcp-tunnels) · [ngrok](https://ngrok.com/docs) | [Tunnel Setup](docs/CONNECT_CHATGPT.md#2-secure-mcp-tunnel--no-inbound-port) |
171
+ ## Securing Remote Access & Client Guides
146
172
 
147
- ### 2. Choose an Authentication Mode
173
+ Once your connection works, **lock down public HTTPS / Tunnel endpoints** so only you can access your gateway:
148
174
 
149
- Authentication is configured independently of how you expose the network:
175
+ ### Add Authentication *(Recommended for Internet-Facing Endpoints)*
150
176
 
151
- | Authentication Mode | When to Use | CLI Command |
152
- | :--- | :--- | :--- |
153
- | **None** *(default)* | Same-computer clients, or tunnels that already handle caller login | `starmux auth mode none` |
154
- | **Bearer API Key** *(recommended)* | Clients that support an `Authorization: Bearer <key>` header | `starmux auth key "My AI Client"` |
155
- | **OAuth 2.0** | Cloud clients that require browser sign-in (e.g., ChatGPT Web) | `starmux auth oauth setup ...` |
177
+ 1. **Bearer API Key (10-second setup for Antigravity, Cursor, WorkBuddy, Kimi Code, Codex, etc.):**
178
+ ```bash
179
+ starmux auth key "My AI Client"
180
+ ```
181
+ Then add `"headers": { "Authorization": "Bearer smx_YOUR_GENERATED_API_KEY" }` to your client's JSON config (or `Authorization: Bearer <key>` in Form UI).
182
+ 2. **OAuth 2.0 Browser Login (for ChatGPT Web):**
183
+ Supports full OAuth 2.0 + PKCE and instant session revocation. See [Security & Authentication — OAuth 2.0](docs/SECURITY.md#4-oauth-20--pkce-oauth-with-keycloak).
156
184
 
157
- ### 3. Client Setup Guides
185
+ ### Tested AI Client Matrix
158
186
 
159
- | AI Client | Recommended Connection & Auth | Status | Setup Guide |
187
+ | AI Client | Recommended Setup | Status | Detailed Guide |
160
188
  | :--- | :--- | :--- | :--- |
161
- | **ChatGPT Web** | Secure Tunnel + OAuth (Browser Login) | ✅ Tested | [ChatGPT OAuth Guide](docs/OAUTH.md) |
162
- | **Claude App** | Remote MCP Custom Connector | 🎯 Target | [Client Compatibility](docs/CLIENTS.md) |
163
- | **Codex** | Protected Tunnel (No Starmux Token) | ✅ Tested | [Remote Setup Guide](docs/REMOTE_SETUP.md) |
164
- | **WorkBuddy** | Direct HTTPS + Bearer API Key | ✅ Tested | [HTTPS + API Key Guide](docs/REMOTE_SETUP.md#2-https--bearer-api-key) |
165
- | **Doubao (豆包)** | HTTPS / Tunnel | ✅ Community Verified | [Client Compatibility](docs/CLIENTS.md) |
166
-
167
- See [Client Compatibility](docs/CLIENTS.md) and [Remote Setup](docs/REMOTE_SETUP.md) for DNS, TLS, Caddy, OAuth, and tunnel configuration details.
189
+ | **ChatGPT Web** | Direct HTTPS or OpenAI Tunnel (+ OAuth) | ✅ Tested | [ChatGPT Web Setup](docs/CLIENTS.md#2-chatgpt-web-tunnel-direct-https--oauth) |
190
+ | **Claude App / Code** | Chat Prompt (`Claude Code`) or Remote MCP Custom Connector (`Claude App`) | ✅ Tested | [AI Coding Agents](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) · [Claude App](docs/CLIENTS.md#5-claude-app--doubao-豆包) |
191
+ | **Antigravity** | Chat Prompt / Local / LAN / SSH / HTTPS | ✅ Tested | [AI Coding Agents Setup](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) |
192
+ | **Codex** | Chat Prompt / Tunnel or HTTPS + Bearer API Key | ✅ Tested | [Codex CLI Setup](docs/CLIENTS.md#4-codex-cli-configtoml) |
193
+ | **Cursor** | Chat Prompt / Local / LAN / SSH / HTTPS | ✅ Tested | [AI Coding Agents Setup](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) |
194
+ | **WorkBuddy** | Chat Prompt / Local / LAN / SSH / HTTPS | ✅ Tested | [AI Coding Agents Setup](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) |
195
+ | **DSH-Harness** | Chat Prompt / Local / LAN / SSH / HTTPS | ✅ Tested | [AI Coding Agents Setup](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) |
196
+ | **Kimi Code** | Chat Prompt / Local / LAN / SSH / HTTPS | ✅ Tested | [AI Coding Agents Setup](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) |
197
+ | **Minimax Code** | Chat Prompt / Local / LAN / SSH / HTTPS | ✅ Tested | [AI Coding Agents Setup](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) |
198
+ | **ZCode** | Chat Prompt / Local / LAN / SSH / HTTPS | ✅ Tested | [AI Coding Agents Setup](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) |
199
+ | **Doubao (豆包)** | Direct HTTPS or Tunnel | ✅ Tested | [Claude & Doubao Setup](docs/CLIENTS.md#5-claude-app--doubao-豆包) |
168
200
 
169
201
  ---
170
202
 
@@ -174,6 +206,7 @@ Manage your gateway with simple, human-friendly commands:
174
206
 
175
207
  ```bash
176
208
  starmux status # Check health, endpoints, and active permission mode
209
+ starmux up . # Switch active project context to current directory
177
210
  starmux auth # View auth mode, active API keys, and OAuth sessions
178
211
  starmux verify # Test local MCP handshake, tool discovery, and workspace read
179
212
  starmux doctor # Run environment and configuration diagnostics
@@ -182,11 +215,9 @@ starmux logs -f # Stream live gateway logs
182
215
  starmux down # Stop the gateway (alias: stop)
183
216
  ```
184
217
 
185
- After initialization, `starmux up` reuses your instance's saved project context. Use `starmux up .` to change it to your current directory, or pass `--foreground` to keep the process in your terminal. Only a new instance defaults to the current directory when no path is supplied.
186
-
187
218
  ### Advanced: Running multiple instances (`-i`)
188
219
 
189
- *(Optional)* By default, Starmux manages a single `default` instance so you never have to pass extra flags. To use different project contexts or permission modes on the same machine, pass `-i <name>` to create instances with their own ports, credentials, configuration, and logs. Instances share the OS account; they are not filesystem sandboxes:
220
+ *(Optional)* By default, Starmux manages a single `default` instance so you never have to pass extra flags. To use different project contexts or permission modes on the same machine, pass `-i <name>` to create instances with their own ports, credentials, configuration, and logs:
190
221
 
191
222
  ```bash
192
223
  starmux up ~/work/repo-a -i work --code
@@ -196,7 +227,7 @@ starmux down -i work # Stop a specific instance
196
227
  starmux rm blog # Stop and archive an instance
197
228
  ```
198
229
 
199
- See [Instances and Configuration](docs/INSTANCES.md) for advanced JSONC configuration (`starmux config edit` / `starmux config apply`) and hot-reload workflows.
230
+ See [Instances and Configuration](docs/INSTANCES.md) for advanced JSONC configuration (`starmux config edit` / `starmux config apply`), mounting extra `stdio` MCP servers, and hot-reload workflows.
200
231
 
201
232
  ---
202
233
 
@@ -223,18 +254,12 @@ The App and CLI manage the same underlying instances. Quitting the App stops onl
223
254
 
224
255
  Starmux began with the [community request for a self-hosted Desktop Commander workbench](https://github.com/wonderwhy-er/DesktopCommanderMCP/issues/714) and is evolving into a universal MCP router and programmable middleware for multi-server environments.
225
256
 
226
- Explore the `docs/` guides by topic:
227
-
228
- - **🌐 Remote Networking & Auth**
229
- - [Remote Setup (HTTPS & Tunnel)](docs/REMOTE_SETUP.md) — Step-by-step Caddy reverse proxy, custom TLS / port `8443`, IPv6, and Tunnel setup.
230
- - [Access Control & API Keys](docs/ACCESS_CONTROL.md) — Creating/revoking Bearer API keys, expiration policies, and using the local Web Access Console.
231
- - [ChatGPT Web & OAuth 2.0](docs/OAUTH.md) & [Connect ChatGPT](docs/CONNECT_CHATGPT.md) — Configuring Keycloak OAuth 2.0 + PKCE and OpenAI Secure MCP Tunnel.
232
- - **⚙️ Advanced Configuration & Security**
233
- - [Multi-Instance & JSONC Config](docs/INSTANCES.md) — Managing instances, hot-reloading `config.jsonc`, mounting extra `stdio` MCP upstreams, and secrets.
234
- - [Security & Permissions](docs/SECURITY.md) — Understanding global Read-Only vs. Coding Mode, upstream permissions, and macOS privacy boundaries.
235
- - [Client Compatibility](docs/CLIENTS.md) & [Platform Details](docs/PLATFORMS.md) — Tested AI client matrix and macOS / Linux / Windows runtime specifics.
236
- - **🛠️ Reference & Project**
237
- - [Documentation Index](docs/README.md) · [Installation](docs/INSTALL.md) · [Contributing](CONTRIBUTING.md)
257
+ Explore the [Documentation Index](docs/README.md) by category:
258
+
259
+ - **📦 [Installation, Platforms & Network Forwarding](docs/INSTALL.md)** — Supported hosts (`macOS`, `Linux`, `NVIDIA Jetson`, `Windows`), everyday CLI commands, upgrades, source builds, and advanced Caddy / Tunnel / SSH forwarding.
260
+ - **🔒 [Security, Access Control & Authentication](docs/SECURITY.md)** — Global Read-Only vs. Coding permissions, Bearer API keys (`starmux auth key`), Web Access Console, and self-hosted Keycloak OAuth 2.0 + PKCE.
261
+ - **🤖 [AI Client Configuration Guides](docs/CLIENTS.md)** — End-to-end setup (including 1-step Prompt Auto-Setup) for **ChatGPT Web**, **Claude App / Code**, **Antigravity**, **Codex**, **Cursor**, **WorkBuddy**, **DSH-Harness**, **Kimi Code**, **Minimax Code**, **ZCode**, and **Doubao (豆包)**.
262
+ - **⚙️ [Multi-Instance & JSONC Configuration](docs/INSTANCES.md)** — Running multiple instances (`-i NAME`), hot-reloading `config.jsonc`, mounting extra local `stdio` MCP servers (`upstreams`), and managing secrets.
238
263
 
239
264
  ## License
240
265
 
@@ -1,4 +1,4 @@
1
- import { ownedByUser, createPrivateDirectory, controlRoot, windows } from "./platform.js";
1
+ import { ownedByUser, createPrivateDirectory, controlRoot, controlSocket, controlSocketRoot, sameFile, windows } from "./platform.js";
2
2
  import { ownerSchema } from "./config-schemas.js";
3
3
  import { GatewayError, errorCode, isSafeInteger, parseJSON, record } from "./validation.js";
4
4
  const CONTROL_VERSION = 1;
@@ -18,7 +18,7 @@ export function controlPaths(port, root = controlRoot()) {
18
18
  deny("CORE_INVALID_PORT");
19
19
  const lock = path.join(root, `core-${port}.lease`);
20
20
  return { root, lock, recovery: lock + ".recovery",
21
- meta: path.join(lock, "owner.json"), socket: windows ? `\\\\.\\pipe\\starmux-${crypto.createHash("sha256").update(path.resolve(lock).toLowerCase()).digest("hex").slice(0, 32)}` : path.join(lock, "control.sock") };
21
+ meta: path.join(lock, "owner.json"), socket: controlSocket(lock) };
22
22
  }
23
23
  function secureDir(p, create = false) {
24
24
  if (create)
@@ -150,6 +150,10 @@ function cleanStale(paths) {
150
150
  deny("CORE_CONTROL_STALE_UNTRUSTED");
151
151
  fs.unlinkSync(item);
152
152
  }
153
+ if (!windows && fs.existsSync(paths.socket)) {
154
+ secureFile(paths.socket, "socket");
155
+ fs.unlinkSync(paths.socket);
156
+ }
153
157
  fs.rmdirSync(paths.lock);
154
158
  }
155
159
  finally {
@@ -229,6 +233,8 @@ export async function acquireControl(port, { root, launcher = "cli" } = {}) {
229
233
  deny("CORE_LAUNCHER_INVALID");
230
234
  const paths = controlPaths(port, root);
231
235
  secureDir(paths.root, true);
236
+ if (!windows)
237
+ createPrivateDirectory(controlSocketRoot());
232
238
  for (let attempt = 0; attempt < 12; attempt++) {
233
239
  try {
234
240
  fs.mkdirSync(paths.lock, { mode: 0o700 });
@@ -351,9 +357,13 @@ export async function acquireControl(port, { root, launcher = "cli" } = {}) {
351
357
  catch { }
352
358
  throw e;
353
359
  }
360
+ const ownsDirectory = sameFile(paths.lock);
361
+ const ownsMetadata = sameFile(paths.meta);
362
+ const ownsSocket = windows ? () => true : sameFile(paths.socket);
354
363
  let releasePromise = null;
355
364
  return {
356
365
  owner,
366
+ intact: () => ownsDirectory() && ownsMetadata() && ownsSocket(),
357
367
  markRunning() { phase = "RUNNING"; },
358
368
  markStopped() { phase = "STOPPED"; },
359
369
  onStop(fn) { onStop = fn; },
@@ -369,19 +379,27 @@ export async function acquireControl(port, { root, launcher = "cli" } = {}) {
369
379
  else
370
380
  resolve(); });
371
381
  });
372
- if (readOwner(paths).runId !== owner.runId)
382
+ // A missing record cannot be reconstructed by a CLI. Its live owner
383
+ // can still release its own unchanged directory without inventing keys.
384
+ if (!fs.existsSync(paths.lock))
385
+ return;
386
+ if (!ownsDirectory())
373
387
  deny("CORE_CONTROL_LEASE_CHANGED");
374
- // node:net automatically unlinks a Unix socket on server.close().
375
- // A second unlink is not required and must not abort lease cleanup.
376
388
  try {
377
- if (!windows)
378
- fs.unlinkSync(paths.socket);
389
+ if (readOwner(paths).runId !== owner.runId)
390
+ deny("CORE_CONTROL_LEASE_CHANGED");
391
+ }
392
+ catch (e) {
393
+ if (errorCode(e) !== "ENOENT")
394
+ throw e;
395
+ }
396
+ try {
397
+ fs.unlinkSync(paths.meta);
379
398
  }
380
399
  catch (e) {
381
400
  if (errorCode(e) !== "ENOENT")
382
401
  throw e;
383
402
  }
384
- fs.unlinkSync(paths.meta);
385
403
  fs.rmdirSync(paths.lock);
386
404
  })();
387
405
  }
@@ -1,19 +1,19 @@
1
- import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
1
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
2
2
  import { createServer, createConnection } from 'node:net';
3
3
  import { fork } from 'node:child_process';
4
- import { mkdir, rm, open } from 'node:fs/promises';
4
+ import { mkdir, rm, open, rmdir } from 'node:fs/promises';
5
5
  import { setTimeout as pause } from 'node:timers/promises';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import path from 'node:path';
8
8
  import { homedir } from 'node:os';
9
9
  import { INSTANCE_CONTROL_VERSION, instanceOwnerSchema, activeConfigSchema, runtimeStateSchema, controlRequestSchema, controlReplySchema, checkInstanceProtocol } from './instance-control-protocol.js';
10
- import { controlRoot, createPrivateDirectory, childEnvKeys, windows } from './platform.js';
10
+ import { controlRoot, controlSocket, controlSocketRoot, sameFile, createPrivateDirectory, childEnvKeys, windows } from './platform.js';
11
11
  import { acquireControl, inspectControlOwner } from './global-core-control.js';
12
12
  import { runtimeIdentity } from './runtime-identity.js';
13
13
  import { atomicPrivate, changeRegistry, availablePort, ensureAccessReady, exists, instanceEnv, instancePaths, loadInstanceConfig, prepareUpstreams, readTrusted, resolveInstance } from './instance-store.js';
14
14
  export function runtimePaths(id) {
15
15
  const root = path.join(controlRoot(), 'i-' + id);
16
- const socket = windows ? '\\\\.\\pipe\\gateway-instance-' + createHash('sha256').update(root).digest('hex').slice(0, 28) : path.join(root, 'control.sock');
16
+ const socket = controlSocket(root);
17
17
  return { root, socket, meta: path.join(root, 'owner.json') };
18
18
  }
19
19
  const signature = (key, message) => createHmac('sha256', key).update(message).digest('hex');
@@ -236,6 +236,8 @@ export async function runInstance(instance, options = {}) {
236
236
  const check = await ownerFor(instance);
237
237
  if (!check || check.runId !== old.runId || alive(check.pid))
238
238
  throw Error('INSTANCE_CONTROL_CHANGED');
239
+ if (!windows)
240
+ await rm(p.socket, { force: true });
239
241
  await rm(p.root, { recursive: true });
240
242
  await mkdir(p.root, { mode: 0o700 });
241
243
  }
@@ -245,6 +247,9 @@ export async function runInstance(instance, options = {}) {
245
247
  }
246
248
  await atomicPrivate(p.meta, JSON.stringify(owner));
247
249
  }, home);
250
+ const ownsDirectory = sameFile(p.root);
251
+ const ownsMetadata = sameFile(p.meta);
252
+ let controlTimer;
248
253
  const state = { protocolVersion: INSTANCE_CONTROL_VERSION, instanceId: instance.id, runId: owner.runId, pid: process.pid, launcher: owner.launcher, appParentPid: parent ?? null, executionMode: options.background ? 'background' : 'foreground', phase: 'starting', port: null, active: null, problem: null, ...identity };
249
254
  let worker = null;
250
255
  let portLease = null;
@@ -482,10 +487,18 @@ export async function runInstance(instance, options = {}) {
482
487
  })();
483
488
  });
484
489
  });
490
+ if (!windows)
491
+ createPrivateDirectory(controlSocketRoot());
485
492
  await new Promise((resolve, reject) => { server.once('error', reject); server.listen(p.socket, () => resolve()); });
493
+ const ownsSocket = windows ? () => true : sameFile(p.socket);
486
494
  portLease = await acquireControl(initial.config.port, { launcher: owner.launcher });
487
495
  portLease.onStop(() => requestStop());
488
496
  await serialize(() => startWorker(active(initial), portLease));
497
+ controlTimer = setInterval(() => {
498
+ if (!ending && (!ownsDirectory() || !ownsMetadata() || !ownsSocket() || portLease && !portLease.intact()))
499
+ void requestStop('INSTANCE_CONTROL_FILES_LOST');
500
+ }, 1000);
501
+ controlTimer.unref();
489
502
  if (options.onReady)
490
503
  await options.onReady();
491
504
  else if (!options.json)
@@ -498,6 +511,8 @@ export async function runInstance(instance, options = {}) {
498
511
  throw error;
499
512
  }
500
513
  finally {
514
+ if (controlTimer)
515
+ clearInterval(controlTimer);
501
516
  if (parentTimer)
502
517
  clearInterval(parentTimer);
503
518
  process.off('SIGTERM', signal);
@@ -516,9 +531,21 @@ export async function runInstance(instance, options = {}) {
516
531
  // Commit completion last. A stop acknowledgement only means accepted;
517
532
  // readers require this snapshot AND release of this run's owner record.
518
533
  await atomicPrivate(path.join(instancePaths(instance, home).snapshots, 'last-runtime.json'), JSON.stringify(state));
519
- const current = await ownerFor(instance);
520
- if (current?.runId === owner.runId)
521
- await rm(p.root, { recursive: true, force: true });
534
+ if (await exists(p.root)) {
535
+ if (!ownsDirectory())
536
+ throw Error('INSTANCE_CONTROL_CHANGED');
537
+ try {
538
+ const current = await ownerFor(instance);
539
+ if (current && current.runId !== owner.runId)
540
+ throw Error('INSTANCE_CONTROL_CHANGED');
541
+ }
542
+ catch (e) {
543
+ if (e.code !== 'ENOENT')
544
+ throw e;
545
+ }
546
+ await rm(p.meta, { force: true });
547
+ await rmdir(p.root);
548
+ }
522
549
  if (state.phase === 'stopped')
523
550
  emit('INSTANCE_STOPPED');
524
551
  }
@@ -58,14 +58,29 @@ export function createPrivateDirectory(folder) {
58
58
  if (!ownedByUser(folder, lstatSync(folder), true))
59
59
  throw Error('PRIVATE_DIRECTORY_UNSAFE');
60
60
  }
61
- export function controlRoot() {
62
- // Local control address contract: keep independent of the package version,
63
- // install path, project directory and display name. A branding edit must not
64
- // silently move the rendezvous point of an already-running supervisor.
61
+ // Identity records are durable state, not disposable temporary files.
62
+ export function controlRoot() { return path.join(homedir(), '.config', 'starmux', 'control'); }
63
+ export function controlSocketRoot() {
65
64
  if (process.platform === 'darwin')
66
65
  return `/private/tmp/starmux-${process.getuid()}`;
67
66
  const id = windows ? createHash('sha256').update(homedir().toLowerCase()).digest('hex').slice(0, 16) : String(process.getuid());
68
- return path.join(tmpdir(), `starmux-${id}`);
67
+ return path.join(windows ? tmpdir() : '/tmp', `starmux-${id}`);
68
+ }
69
+ // Hash the durable address: short Unix socket paths even with a long HOME,
70
+ // and separate endpoints for distinct accounts, homes and test control roots.
71
+ export function controlSocket(address) {
72
+ const id = createHash('sha256').update(path.resolve(address)).digest('hex').slice(0, 32);
73
+ return windows ? '\\\\.\\pipe\\starmux-' + id : path.join(controlSocketRoot(), id + '.sock');
74
+ }
75
+ export function sameFile(file) {
76
+ const original = lstatSync(file);
77
+ return () => { try {
78
+ const now = lstatSync(file);
79
+ return !now.isSymbolicLink() && now.dev === original.dev && now.ino === original.ino;
80
+ }
81
+ catch {
82
+ return false;
83
+ } };
69
84
  }
70
85
  export function within(candidate, root) {
71
86
  const relative = path.relative(root, candidate);
package/package.json CHANGED
@@ -1,7 +1,27 @@
1
1
  {
2
2
  "name": "starmux",
3
- "version": "0.15.0",
4
- "description": "Connect AI clients to files, terminals and development tools on your own computer.",
3
+ "version": "0.16.1",
4
+ "description": "Self-hosted remote MCP gateway for ChatGPT, WorkBuddy, Codex and AI agents to access files, terminals and Desktop Commander tools on Linux, NVIDIA Jetson, macOS and Windows.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "mcp-gateway",
9
+ "mcp-server",
10
+ "remote-mcp",
11
+ "self-hosted",
12
+ "chatgpt",
13
+ "workbuddy",
14
+ "desktop-commander",
15
+ "claude-code",
16
+ "codex",
17
+ "cursor",
18
+ "ai-agents",
19
+ "computer-use",
20
+ "linux",
21
+ "nvidia-jetson",
22
+ "streamable-http",
23
+ "mcp-multiplexer"
24
+ ],
5
25
  "license": "MIT",
6
26
  "homepage": "https://github.com/cookyecat/starmux#readme",
7
27
  "repository": {