@cordfuse/llmux 0.12.0 → 0.12.2

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/README.md CHANGED
@@ -1,119 +1,208 @@
1
1
  # llmux
2
2
 
3
- You have Claude Code in one terminal, Codex in another, aider in a third, and
4
- opencode in a fourth. They're all live, all mid-conversation, all costing
3
+ You have Claude Code in one terminal, Codex in another, Aider in a third, and
4
+ OpenCode in a fourth. They're all live, all mid-conversation, all costing
5
5
  nothing to keep open. But to fire a prompt at one of them you have to find the
6
- right window, click in, and type. To fire the same prompt at two of them — you
7
- just don't. And if you're on the couch with your phone? Forget it.
6
+ right window, click in, and type. To do anything from your phone? Forget it.
8
7
 
9
8
  llmux turns every agent CLI into a named tmux session you can drive from
10
- anywhere. Spawn `claude`, `agy`, `codex`, `qwen`, `opencode`, `grok`, `aider`,
11
- `goose`, and `gh copilot` once. Then fire prompts at any of them — by name,
12
- broadcast to several at once, from a REST call, a curl one-liner, or a browser
13
- terminal on your phone over Tailscale.
9
+ anywhere. Spawn `claude`, `codex`, `agy`, `gemini`, `qwen`, `opencode`, `amp`,
10
+ `grok`, `aider`, `continue`, `kiro`, `cursor`, `plandex`, `goose`, or
11
+ `gh copilot` once. Then fire prompts at any of them by name, from a CLI, from
12
+ a REST call, or from a browser on your phone over Tailscale. Past
13
+ conversations are browsable and resumable. The agents keep running.
14
14
 
15
- The agents keep running. You stop window-hopping.
15
+ > **Status:** v0.12.2 daemon + CLI client consolidated into one binary
16
+ > (`llmux`). Auth, tokens, mobile picker, conversation resume, Claude Code
17
+ > history adapter shipped. See [CHANGELOG.md](./CHANGELOG.md).
16
18
 
17
- > **Status:** v0.2.1 — Phases 0, 1, and 4 shipped. Phases 2, 3, 5, 6, 7 pending.
18
- > See [CHANGELOG.md](./CHANGELOG.md).
19
+ <p align="center">
20
+ <img src="https://raw.githubusercontent.com/cordfuse/llmux/main/docs/screenshots/sessions.jpg" width="32%" alt="mobile sessions picker — 5 agents running, respawn/edit/kill per row">
21
+ <img src="https://raw.githubusercontent.com/cordfuse/llmux/main/docs/screenshots/edit.jpg" width="32%" alt="edit session form — agent, name, cwd, flags, env vars">
22
+ <img src="https://raw.githubusercontent.com/cordfuse/llmux/main/docs/screenshots/chat.jpg" width="32%" alt="phone chat — xterm.js with soft-keyboard toolbar attached to an OpenCode session">
23
+ </p>
24
+
25
+ > Browser picker, edit form, and attached terminal — all on a phone over
26
+ > Tailscale HTTPS.
19
27
 
20
28
  ## Install
21
29
 
22
30
  ```bash
23
- # On the machine with tmux + your agent CLIs
24
- npm install -g @cordfuse/llmuxd
25
-
26
- # Anywhere you want to send prompts from (laptop, phone, CI)
31
+ # One package, one binary — installs on the daemon host AND any client machine
27
32
  npm install -g @cordfuse/llmux
28
33
  ```
29
34
 
35
+ If you used the now-deprecated `@cordfuse/llmuxd` package: uninstall it and
36
+ install `@cordfuse/llmux` instead. The `llmuxd` binary is gone; the `llmux`
37
+ binary covers both daemon and client roles.
38
+
30
39
  ## 30-second quickstart
31
40
 
32
41
  ```bash
33
- # Spawn an agent in a named tmux session
34
- llmuxd spawn claude --name main --cwd ~/projects/myapp
42
+ # 1. Start the daemon (binds REST + WebSocket + browser picker)
43
+ llmux server start --port 3030
44
+
45
+ # 2. Spawn an agent into a named tmux session
46
+ llmux session start claude --name main --cwd ~/projects/myapp
35
47
 
36
- # Fire a prompt at it — fire-and-forget
37
- llmuxd send main "what does src/index.ts do?"
48
+ # 3. Fire a prompt — fire-and-forget
49
+ llmux session prompt main "what does src/index.ts do?"
38
50
 
39
- # Or attach interactively (switch-client if you're already in tmux)
40
- llmuxd attach main
51
+ # 4. Or attach interactively (raw TTY pass-through)
52
+ llmux session attach main
41
53
 
42
- # Or expose a browser-terminal opens the session in any browser
43
- llmuxd serve
54
+ # 5. Or open the browser picker (URL is in the server start banner)
55
+ # Pick a session, get a full-screen xterm.js terminal wired over WebSocket.
44
56
  ```
45
57
 
46
- `llmuxd serve` prints reachable URLs (Local, LAN, Tailscale). Open one, pick a
47
- session, and you get a full-screen xterm.js terminal wired to your live tmux
48
- session over WebSocket. Multiple browsers can attach to the same session — tmux
49
- handles the multiplexing. On mobile, the floating toolbar gives you arrow keys,
50
- modifiers, and shell chars `gboard` hides.
58
+ On mobile the picker is a real PWA-style surface spawn / restart / kill /
59
+ edit / resume past conversations, with a confirmation modal on destructive
60
+ actions. The chat page is a phone-friendly xterm with a custom soft-keyboard
61
+ toolbar that surfaces Esc / Tab / Ctrl / Alt / arrows / shell chars that
62
+ gboard hides.
51
63
 
52
- > **Auth:** `serve` runs without authentication today. Phase 3 lands SAS tokens.
53
- > Until then, bind to `127.0.0.1` (default) or expose only over Tailscale.
64
+ ## Remote operation
54
65
 
55
- ## How it works
66
+ The same binary is the client. Set `--server` (or `LLMUX_SERVER` env) on any
67
+ session/agent verb and it routes over HTTP instead of operating locally:
56
68
 
57
- Two packages:
69
+ ```bash
70
+ export LLMUX_SERVER=http://100.105.221.46:3030
71
+ export LLMUX_TOKEN=sas_… # mint with `llmux token create`
58
72
 
59
- | Package | Where it runs | What it does |
60
- |---|---|---|
61
- | `@cordfuse/llmuxd` | The machine with tmux | Daemon: session management, REST API, web terminal |
62
- | `@cordfuse/llmux` | Anywhere | Thin HTTP client no tmux dependency |
73
+ llmux session list
74
+ llmux session prompt main "tomorrow's plan?"
75
+ llmux session attach main # raw TTY pass-through over WS
76
+ llmux session resume main --latest # rebind to the most recent claude convo
77
+ ```
78
+
79
+ Localhost requests bypass auth; remote requests require a Bearer token.
80
+ `--token <sas>` per-command works too.
63
81
 
64
- Each spawned agent is a real tmux session, not a wrapped PTY. llmuxd dispatches
65
- input via `tmux send-keys` and reads output by attaching xterm.js over a
66
- WebSocket bridge. That keeps the agent CLIs unmodified — Claude Code is still
67
- running Claude Code; llmuxd just coordinates input and exposes the surface.
82
+ ## Noun-prefix surface
68
83
 
69
- Session metadata lives at `$XDG_STATE_HOME/llmuxd/sessions.json` (default
70
- `~/.local/state/llmuxd/sessions.json`, `0600` perms, versioned schema).
71
- Reconciliation is on demand — sessions can die outside llmuxd, and `status`
72
- reports live tmux state.
84
+ ```
85
+ session list / start / stop / restart / attach / prompt / broadcast
86
+ / resume / history
87
+ server start
88
+ token create / list / revoke
89
+ agent list [--all] [--installed] [--json]
90
+ ```
73
91
 
74
- The daemon runs on Node (not Bun) because `node-pty`'s native prebuilds target
75
- Node; Bun caused immediate SIGHUP on the PTY child.
92
+ Global flags: `--server <url>`, `--token <sas>`, `--help`, `--version`.
93
+
94
+ Backward-compat shims (kept one release): `llmux serve`, `llmux ls`,
95
+ `llmux status`, and the legacy flat verbs (`llmux send`, `llmux spawn`,
96
+ `llmux kill`, etc.) still work; they fall through to the noun-prefix
97
+ dispatcher.
98
+
99
+ ## How it works
100
+
101
+ Each spawned agent is a real tmux session, not a wrapped PTY. The daemon
102
+ dispatches input via `tmux send-keys` and exposes the surface over a REST API
103
+ plus a WebSocket bridge to xterm.js (via node-pty attached to
104
+ `tmux attach -t <name>`). That keeps the agent CLIs unmodified — Claude Code
105
+ is still running Claude Code; llmux just coordinates input and exposes the
106
+ surface.
107
+
108
+ State lives at `~/.local/state/llmuxd/sessions.json` (or
109
+ `$XDG_STATE_HOME/llmuxd/sessions.json`) with `0600` perms and a versioned
110
+ schema. Auth tokens live in the sibling `auth.json`. The state directory keeps
111
+ its `llmuxd/` name across the v0.12.0 package consolidation so existing
112
+ operators don't need to migrate anything.
113
+
114
+ The daemon runs on Node (not Bun) — `node-pty`'s native prebuilds target
115
+ Node, and attaching to tmux through node-pty under Bun caused immediate SIGHUP.
76
116
 
77
117
  ## Supported agents
78
118
 
79
- | Session key | CLI |
80
- |---|---|
81
- | `claude` | [Claude Code](https://github.com/anthropics/claude-code) |
82
- | `agy` | [Antigravity CLI](https://antigravity.dev) |
83
- | `codex` | [OpenAI Codex CLI](https://github.com/openai/codex) |
84
- | `qwen` | [Qwen Code](https://github.com/QwenLM/qwen-code) |
85
- | `opencode` | [opencode](https://opencode.ai) |
86
- | `grok` | [Grok Build CLI](https://x.ai) |
87
- | `aider` | [aider](https://aider.chat) |
88
- | `goose` | [Goose](https://block.github.io/goose/) |
89
- | `copilot` | [GitHub Copilot CLI](https://github.com/github/gh-copilot) (via `gh` extension) |
90
-
91
- Only installed agents are spawnable. llmuxd uses `command -v` (and
92
- `gh extension list` for copilot) to detect availability.
119
+ | Key | CLI | Danger-mode default |
120
+ |---|---|---|
121
+ | `claude` | [Claude Code](https://docs.claude.com/en/docs/claude-code/overview) | `--dangerously-skip-permissions` |
122
+ | `codex` | [OpenAI Codex CLI](https://github.com/openai/codex) | `--dangerously-bypass-approvals-and-sandbox` |
123
+ | `agy` | [Antigravity CLI](https://antigravity.google) | `--dangerously-skip-permissions` |
124
+ | `gemini` | [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `--yolo` |
125
+ | `qwen` | [Qwen Code](https://github.com/QwenLM/qwen-code) | `--yolo` |
126
+ | `opencode` | [OpenCode](https://opencode.ai) | env: `OPENCODE_YOLO=1` (TUI lacks a flag) |
127
+ | `amp` | [Sourcegraph Amp](https://ampcode.com) | `--dangerously-allow-all` |
128
+ | `grok` | [Grok Build CLI](https://x.ai/cli) | `--always-approve` |
129
+ | `aider` | [Aider](https://aider.chat) | `--yes-always` |
130
+ | `continue` | [Continue CLI](https://docs.continue.dev/guides/cli) (`cn`) | `--auto` |
131
+ | `kiro` | [Kiro CLI](https://kiro.dev/cli/) | `--trust-all-tools` |
132
+ | `cursor` | [Cursor CLI](https://cursor.com/docs/cli/installation) (`cursor-agent`) | (config-based) |
133
+ | `plandex` | [Plandex](https://plandex.ai) | (interactive `set-auto`) |
134
+ | `goose` | [Goose](https://block.github.io/goose) | env: `GOOSE_MODE=auto` |
135
+ | `copilot` | [GitHub Copilot CLI](https://docs.github.com/en/copilot/how-tos/use-copilot-in-the-cli) (`gh copilot`) | n/a |
136
+
137
+ Only installed agents appear in `llmux agent list` and the picker dropdown.
138
+ Detection uses a pure-Node PATH walk for most; `copilot` checks the gh-managed
139
+ binary directory.
140
+
141
+ Per-session overrides via `llmux session start <agent>`:
142
+ - `--name <X>` — tmux session name (defaults to the agent key)
143
+ - `--cwd <path>` — working directory (accepts `~/…` shorthand)
144
+ - `--flags "<f>"` — replace the agent's default flags entirely
145
+ - `--env "KEY=VAL"` — extra env vars (newline-separated for multiple)
146
+
147
+ Editing any of these on a running session via the web picker auto-respawns
148
+ the tmux session so changes take effect immediately.
149
+
150
+ ## Conversation resume
151
+
152
+ For agents with a history adapter (Claude Code today; codex/gemini/etc.
153
+ coming), the row gets a `☰ N` button. Tap it to see past conversations in the
154
+ session's cwd; pick one to relaunch the agent with its `--resume <id>` flag.
155
+ State preserves the binding across restarts so respawn keeps you on the
156
+ same conversation. Use `llmux session resume <name> --latest` from the CLI
157
+ for the same flow.
158
+
159
+ ## Auth
160
+
161
+ `llmux server start` runs without auth until you create a token:
162
+
163
+ ```bash
164
+ llmux token create --name phone
165
+ # prints sas_…<43-char-base64url> once; copy it.
166
+ # pass --qr-endpoint tailscale-https for a QR-code deep-link that logs you
167
+ # in on first scan from a phone.
168
+
169
+ llmux token list
170
+ llmux token revoke <8-char-id>
171
+ ```
172
+
173
+ After the first token exists, all non-localhost HTTP/WS requests require
174
+ either `Authorization: Bearer <sas>` (CLI / curl) or the `llmuxd_token`
175
+ cookie set by the browser gate. Localhost stays open so local CLI use needs
176
+ no token.
177
+
178
+ If `tailscale serve --https=443 http://localhost:<port>` is configured on the
179
+ host, the server-start banner surfaces the HTTPS hostname URL above the
180
+ http endpoints. The browser picker is a clean TLS surface; CLI `attach`
181
+ currently speaks ws:// only.
93
182
 
94
183
  ## Config (`.llmux.yaml`)
95
184
 
96
- llmuxd looks for config in this order (highest priority first):
185
+ A YAML config (project-local or global) can override per-agent defaults.
186
+ Discovery order:
97
187
 
98
188
  1. `--config <path>` flag
99
- 2. `./.llmux.yaml` (project-level, auto-discovered in cwd)
189
+ 2. `./.llmux.yaml` (project-local, auto-discovered in cwd)
100
190
  3. `~/.config/llmux/config.yaml` (global default)
101
191
  4. `LLMUX_CONFIG=<path>` env var
102
192
 
103
- All config has sensible defaultsllmuxd runs without any YAML file.
104
-
105
- See [docs/config.md](./docs/config.md) (forthcoming) for the full schema.
193
+ llmux runs without any YAML file all defaults are baked into
194
+ `agents.ts`. The `init` command to generate a starter YAML is not yet
195
+ shipped; create one by hand if you want to override defaults today.
106
196
 
107
- ## Build phases
197
+ ## Environment
108
198
 
109
- - [x] **Phase 0** — scaffold, CLI stubs *(v0.0.1)*
110
- - [x] **Phase 1** — spawn / send / broadcast / chat / kill / status *(v0.1.0)*
111
- - [ ] **Phase 2** `.llmux.yaml` config + `llmuxd init`
112
- - [ ] **Phase 3** — REST API + `llmux` HTTP client + SAS tokens
113
- - [x] **Phase 4** web terminal (xterm.js + node-pty + WebSocket) + mobile UX *(v0.2.0–v0.2.1)*
114
- - [ ] **Phase 5** QR codes + serve UX + service templates (systemd/launchd)
115
- - [ ] **Phase 6** agent-initiated spawning (`LLMUX_SERVER` / `LLMUX_TOKEN` auto-inject)
116
- - [ ] **Phase 7** — polish + npm publish
199
+ | Variable | Purpose |
200
+ |---|---|
201
+ | `LLMUX_SERVER` | Default `--server` URL for session/agent verbs |
202
+ | `LLMUX_TOKEN` | Default `--token` SAS auth |
203
+ | `LLMUX_PORT` | Default port resolution for QR-endpoint helpers |
204
+ | `XDG_STATE_HOME` | Override for the state directory parent |
205
+ | `OPENCODE_YOLO`, `GOOSE_MODE`, | Forwarded by `envDefaults` per-agent |
117
206
 
118
207
  ## License
119
208
 
package/dist/index.js CHANGED
@@ -753,7 +753,7 @@ function pickerPage() {
753
753
  </div>
754
754
  </div>
755
755
  <footer>
756
- <span>llmuxd v${escapeHtml(DAEMON_VERSION)}</span>
756
+ <span>llmux v${escapeHtml(DAEMON_VERSION)}</span>
757
757
  ${authEnabled() ? `<span class="ok">\u2713 auth required \u2014 ${listAuthTokens().length} active token${listAuthTokens().length === 1 ? "" : "s"}</span>` : `<span class="warn">\u26A0 no auth \u2014 anyone on the network can attach</span>`}
758
758
  </footer>
759
759
  <script>
@@ -810,7 +810,7 @@ function pickerPage() {
810
810
 
811
811
  function render(sessions){
812
812
  if (!sessions || sessions.length === 0){
813
- container.innerHTML = '<div class="empty">no sessions yet \u2014 spawn one from the CLI:<br><br><code>llmuxd spawn claude --name <em>name</em></code></div>';
813
+ container.innerHTML = '<div class="empty">no sessions yet \u2014 spawn one from the CLI:<br><br><code>llmux session start claude --name <em>name</em></code></div>';
814
814
  return;
815
815
  }
816
816
  const rows = sessions.map(rowHtml).join('');
@@ -1287,7 +1287,7 @@ function pickerPage() {
1287
1287
  }
1288
1288
  function renderSessionTable(sessions) {
1289
1289
  if (sessions.length === 0) {
1290
- return `<div class="empty">no sessions yet \u2014 spawn one from the CLI:<br><br><code>llmuxd spawn claude --name <em>name</em></code></div>`;
1290
+ return `<div class="empty">no sessions yet \u2014 spawn one from the CLI:<br><br><code>llmux session start claude --name <em>name</em></code></div>`;
1291
1291
  }
1292
1292
  const rows = sessions.map((s) => {
1293
1293
  const cls = `state-${s.status}`;
@@ -1386,7 +1386,7 @@ function sessionPage(name) {
1386
1386
  const jsonVersion = JSON.stringify(DAEMON_VERSION);
1387
1387
  return `<!doctype html><html lang="en"><head>
1388
1388
  <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,interactive-widget=resizes-content">
1389
- <title>${escapedName} \u2014 llmuxd</title>
1389
+ <title>${escapedName} \u2014 llmux</title>
1390
1390
  <link rel="icon" href="${FAVICON_DATA_URL}">
1391
1391
  <link rel="apple-touch-icon" href="${FAVICON_DATA_URL}">
1392
1392
  <link rel="stylesheet" href="${XTERM_CSS}">
@@ -1901,10 +1901,10 @@ function isWsAuthorized(req, urlSearch) {
1901
1901
  return validateAuthToken(extractWsToken(req, urlSearch));
1902
1902
  }
1903
1903
  function gatePage(reason) {
1904
- const message = reason === "invalid" ? "Token rejected. Try again." : "This llmuxd instance requires a token.";
1904
+ const message = reason === "invalid" ? "Token rejected. Try again." : "This llmux daemon requires a token.";
1905
1905
  return `<!doctype html><html lang="en"><head>
1906
1906
  <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
1907
- <title>llmuxd \u2014 auth</title>
1907
+ <title>llmux \u2014 auth</title>
1908
1908
  <link rel="icon" href="${FAVICON_DATA_URL}">
1909
1909
  <style>
1910
1910
  :root{color-scheme:dark}
@@ -1935,7 +1935,7 @@ function gatePage(reason) {
1935
1935
  <div class="msg" id="msg"></div>
1936
1936
  </form>
1937
1937
  <div class="hint">
1938
- Generate a token on the daemon host: <code>llmuxd token create</code><br>
1938
+ Generate a token on the daemon host: <code>llmux token create</code><br>
1939
1939
  The token is sent as a cookie after unlock. Localhost bypasses this gate.
1940
1940
  </div>
1941
1941
  </div>
@@ -2538,7 +2538,7 @@ function attachSession(ws, sessionName) {
2538
2538
  });
2539
2539
  }
2540
2540
  function printBanner(port) {
2541
- console.log(`llmuxd v${DAEMON_VERSION}
2541
+ console.log(`llmux v${DAEMON_VERSION}
2542
2542
  `);
2543
2543
  const addrs = getAddresses(port);
2544
2544
  const width = Math.max(10, ...addrs.map((a) => a.label.length + 2));
@@ -2553,7 +2553,7 @@ function printBanner(port) {
2553
2553
  } else {
2554
2554
  console.log(`
2555
2555
  \u26A0 running without auth \u2014 anyone on the network can attach.`);
2556
- console.log(` create a token with \`llmuxd token create\` to enable auth.
2556
+ console.log(` create a token with \`llmux token create\` to enable auth.
2557
2557
  `);
2558
2558
  }
2559
2559
  }
@@ -2645,7 +2645,7 @@ function handleStatus(args) {
2645
2645
  return;
2646
2646
  }
2647
2647
  if (tracked.length === 0) {
2648
- console.log("no llmuxd sessions");
2648
+ console.log("no llmux sessions");
2649
2649
  return;
2650
2650
  }
2651
2651
  const rows = tracked.map((s) => [
@@ -2670,7 +2670,7 @@ function handleSend(args) {
2670
2670
  const prompt = promptParts.join(" ");
2671
2671
  const { session } = resolveTarget(target);
2672
2672
  if (!hasSession(session.name)) {
2673
- throw new Error(`session "${session.name}" is in state but not live in tmux (exited?). Try \`llmuxd respawn ${session.name}\`.`);
2673
+ throw new Error(`session "${session.name}" is in state but not live in tmux (exited?). Try \`llmux session restart ${session.name}\`.`);
2674
2674
  }
2675
2675
  sendKeys(session.name, prompt, { enter: true });
2676
2676
  console.log(`sent ${prompt.length} bytes \u2192 ${session.name}`);
@@ -2704,7 +2704,7 @@ function handleChat(args) {
2704
2704
  const target = args.positional[0];
2705
2705
  if (!target) throw new Error("chat requires <session>");
2706
2706
  if (args.flags.browser) {
2707
- throw new Error("--browser requires `llmuxd serve` (Phase 4). Use `llmuxd chat` without --browser for now.");
2707
+ throw new Error("--browser requires the web server (`llmux server start`). Without --browser, use `llmux session attach` for raw TTY pass-through.");
2708
2708
  }
2709
2709
  const { session } = resolveTarget(target);
2710
2710
  if (!hasSession(session.name)) {
@@ -2749,7 +2749,7 @@ function resolveQrEndpoint(selector, port) {
2749
2749
  }
2750
2750
  if (matches.length > 1) {
2751
2751
  throw new Error(
2752
- `--qr-endpoint "${selector}" is ambiguous (${matches.length} matches). Use \`llmuxd token create --qr\` without an endpoint to pick interactively.`
2752
+ `--qr-endpoint "${selector}" is ambiguous (${matches.length} matches). Use \`llmux token create --qr\` without an endpoint to pick interactively.`
2753
2753
  );
2754
2754
  }
2755
2755
  return matches[0];
@@ -2823,7 +2823,7 @@ function handleTokenShow(args) {
2823
2823
  return;
2824
2824
  }
2825
2825
  if (tokens.length === 0) {
2826
- console.log("no tokens \u2014 auth is disabled. Create one with `llmuxd token create`.");
2826
+ console.log("no tokens \u2014 auth is disabled. Create one with `llmux token create`.");
2827
2827
  return;
2828
2828
  }
2829
2829
  const headers = ["ID", "NAME", "CREATED", "EXPIRES"];
@@ -2913,7 +2913,7 @@ function help(name, summary, usage) {
2913
2913
  ` ${usage}`,
2914
2914
  "",
2915
2915
  "Environment:",
2916
- " LLMUX_SERVER base URL of the llmuxd daemon (e.g. http://localhost:3030)",
2916
+ " LLMUX_SERVER base URL of the llmux daemon (e.g. http://localhost:3030)",
2917
2917
  " LLMUX_TOKEN auth token (sas_\u2026); not required for localhost",
2918
2918
  ""
2919
2919
  ].join("\n");
@@ -2921,7 +2921,7 @@ function help(name, summary, usage) {
2921
2921
  function resolveContext() {
2922
2922
  const baseUrl = process.env.LLMUX_SERVER;
2923
2923
  if (!baseUrl) {
2924
- throw new Error("LLMUX_SERVER is not set. Point it at your llmuxd (e.g. http://localhost:3030).");
2924
+ throw new Error("LLMUX_SERVER is not set. Point it at your llmux daemon (e.g. http://localhost:3030).");
2925
2925
  }
2926
2926
  return { baseUrl: baseUrl.replace(/\/$/, ""), token: process.env.LLMUX_TOKEN };
2927
2927
  }
@@ -2940,7 +2940,7 @@ async function request(ctx, method, path, body) {
2940
2940
  }
2941
2941
  if (r.status === 401) {
2942
2942
  throw new Error(
2943
- "unauthorized \u2014 set LLMUX_TOKEN (use `llmuxd token create` on the daemon host to mint one)"
2943
+ "unauthorized \u2014 set LLMUX_TOKEN (use `llmux token create` on the daemon host to mint one)"
2944
2944
  );
2945
2945
  }
2946
2946
  if (r.status === 404) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cordfuse/llmux",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "tmux-based AI agent dispatcher — REST/WS daemon + CLI client in one binary",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/cli.ts CHANGED
@@ -95,6 +95,6 @@ export function renderFlagHelp(specs: FlagSpecs): string {
95
95
  }
96
96
 
97
97
  export function notImplemented(commandPath: string): never {
98
- console.error(`llmuxd ${commandPath}: not yet implemented (scaffold)`);
98
+ console.error(`llmux ${commandPath}: not yet implemented (scaffold)`);
99
99
  process.exit(70);
100
100
  }
@@ -18,7 +18,7 @@ function help(name: string, summary: string, usage: string): () => string {
18
18
  ` ${usage}`,
19
19
  '',
20
20
  'Environment:',
21
- ' LLMUX_SERVER base URL of the llmuxd daemon (e.g. http://localhost:3030)',
21
+ ' LLMUX_SERVER base URL of the llmux daemon (e.g. http://localhost:3030)',
22
22
  ' LLMUX_TOKEN auth token (sas_…); not required for localhost',
23
23
  '',
24
24
  ].join('\n');
@@ -32,7 +32,7 @@ interface ClientContext {
32
32
  export function resolveContext(): ClientContext {
33
33
  const baseUrl = process.env.LLMUX_SERVER;
34
34
  if (!baseUrl) {
35
- throw new Error('LLMUX_SERVER is not set. Point it at your llmuxd (e.g. http://localhost:3030).');
35
+ throw new Error('LLMUX_SERVER is not set. Point it at your llmux daemon (e.g. http://localhost:3030).');
36
36
  }
37
37
  return { baseUrl: baseUrl.replace(/\/$/, ''), token: process.env.LLMUX_TOKEN };
38
38
  }
@@ -62,7 +62,7 @@ async function request<T = unknown>(
62
62
  }
63
63
  if (r.status === 401) {
64
64
  throw new Error(
65
- 'unauthorized — set LLMUX_TOKEN (use `llmuxd token create` on the daemon host to mint one)',
65
+ 'unauthorized — set LLMUX_TOKEN (use `llmux token create` on the daemon host to mint one)',
66
66
  );
67
67
  }
68
68
  if (r.status === 404) {
@@ -119,7 +119,7 @@ export function handleStatus(args: ParsedArgs): void {
119
119
  }
120
120
 
121
121
  if (tracked.length === 0) {
122
- console.log('no llmuxd sessions');
122
+ console.log('no llmux sessions');
123
123
  return;
124
124
  }
125
125
 
@@ -146,7 +146,7 @@ export function handleSend(args: ParsedArgs): void {
146
146
  const prompt = promptParts.join(' ');
147
147
  const { session } = resolveTarget(target);
148
148
  if (!tmux.hasSession(session.name)) {
149
- throw new Error(`session "${session.name}" is in state but not live in tmux (exited?). Try \`llmuxd respawn ${session.name}\`.`);
149
+ throw new Error(`session "${session.name}" is in state but not live in tmux (exited?). Try \`llmux session restart ${session.name}\`.`);
150
150
  }
151
151
  tmux.sendKeys(session.name, prompt, { enter: true });
152
152
  console.log(`sent ${prompt.length} bytes → ${session.name}`);
@@ -182,7 +182,7 @@ export function handleChat(args: ParsedArgs): void {
182
182
  const target = args.positional[0];
183
183
  if (!target) throw new Error('chat requires <session>');
184
184
  if (args.flags.browser) {
185
- throw new Error('--browser requires `llmuxd serve` (Phase 4). Use `llmuxd chat` without --browser for now.');
185
+ throw new Error('--browser requires the web server (`llmux server start`). Without --browser, use `llmux session attach` for raw TTY pass-through.');
186
186
  }
187
187
  const { session } = resolveTarget(target);
188
188
  if (!tmux.hasSession(session.name)) {
@@ -236,7 +236,7 @@ function resolveQrEndpoint(selector: string, port: number): { label: string; url
236
236
  }
237
237
  if (matches.length > 1) {
238
238
  throw new Error(
239
- `--qr-endpoint "${selector}" is ambiguous (${matches.length} matches). Use \`llmuxd token create --qr\` without an endpoint to pick interactively.`,
239
+ `--qr-endpoint "${selector}" is ambiguous (${matches.length} matches). Use \`llmux token create --qr\` without an endpoint to pick interactively.`,
240
240
  );
241
241
  }
242
242
  return matches[0]!;
@@ -320,7 +320,7 @@ export function handleTokenShow(args: ParsedArgs): void {
320
320
  return;
321
321
  }
322
322
  if (tokens.length === 0) {
323
- console.log('no tokens — auth is disabled. Create one with `llmuxd token create`.');
323
+ console.log('no tokens — auth is disabled. Create one with `llmux token create`.');
324
324
  return;
325
325
  }
326
326
  const headers = ['ID', 'NAME', 'CREATED', 'EXPIRES'];
@@ -314,7 +314,7 @@ function pickerPage(): string {
314
314
  </div>
315
315
  </div>
316
316
  <footer>
317
- <span>llmuxd v${escapeHtml(DAEMON_VERSION)}</span>
317
+ <span>llmux v${escapeHtml(DAEMON_VERSION)}</span>
318
318
  ${authStore.authEnabled()
319
319
  ? `<span class="ok">✓ auth required — ${authStore.listAuthTokens().length} active token${authStore.listAuthTokens().length === 1 ? '' : 's'}</span>`
320
320
  : `<span class="warn">⚠ no auth — anyone on the network can attach</span>`}
@@ -373,7 +373,7 @@ function pickerPage(): string {
373
373
 
374
374
  function render(sessions){
375
375
  if (!sessions || sessions.length === 0){
376
- container.innerHTML = '<div class="empty">no sessions yet — spawn one from the CLI:<br><br><code>llmuxd spawn claude --name <em>name</em></code></div>';
376
+ container.innerHTML = '<div class="empty">no sessions yet — spawn one from the CLI:<br><br><code>llmux session start claude --name <em>name</em></code></div>';
377
377
  return;
378
378
  }
379
379
  const rows = sessions.map(rowHtml).join('');
@@ -851,7 +851,7 @@ function pickerPage(): string {
851
851
 
852
852
  function renderSessionTable(sessions: SessionView[]): string {
853
853
  if (sessions.length === 0) {
854
- return `<div class="empty">no sessions yet — spawn one from the CLI:<br><br><code>llmuxd spawn claude --name <em>name</em></code></div>`;
854
+ return `<div class="empty">no sessions yet — spawn one from the CLI:<br><br><code>llmux session start claude --name <em>name</em></code></div>`;
855
855
  }
856
856
  const rows = sessions
857
857
  .map((s) => {
@@ -956,7 +956,7 @@ function sessionPage(name: string): string {
956
956
  const jsonVersion = JSON.stringify(DAEMON_VERSION);
957
957
  return `<!doctype html><html lang="en"><head>
958
958
  <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover,interactive-widget=resizes-content">
959
- <title>${escapedName} — llmuxd</title>
959
+ <title>${escapedName} — llmux</title>
960
960
  <link rel="icon" href="${FAVICON_DATA_URL}">
961
961
  <link rel="apple-touch-icon" href="${FAVICON_DATA_URL}">
962
962
  <link rel="stylesheet" href="${XTERM_CSS}">
@@ -1481,10 +1481,10 @@ function isWsAuthorized(req: IncomingMessage, urlSearch: URLSearchParams): boole
1481
1481
 
1482
1482
  function gatePage(reason: 'missing' | 'invalid'): string {
1483
1483
  const message =
1484
- reason === 'invalid' ? 'Token rejected. Try again.' : 'This llmuxd instance requires a token.';
1484
+ reason === 'invalid' ? 'Token rejected. Try again.' : 'This llmux daemon requires a token.';
1485
1485
  return `<!doctype html><html lang="en"><head>
1486
1486
  <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
1487
- <title>llmuxd — auth</title>
1487
+ <title>llmux — auth</title>
1488
1488
  <link rel="icon" href="${FAVICON_DATA_URL}">
1489
1489
  <style>
1490
1490
  :root{color-scheme:dark}
@@ -1515,7 +1515,7 @@ function gatePage(reason: 'missing' | 'invalid'): string {
1515
1515
  <div class="msg" id="msg"></div>
1516
1516
  </form>
1517
1517
  <div class="hint">
1518
- Generate a token on the daemon host: <code>llmuxd token create</code><br>
1518
+ Generate a token on the daemon host: <code>llmux token create</code><br>
1519
1519
  The token is sent as a cookie after unlock. Localhost bypasses this gate.
1520
1520
  </div>
1521
1521
  </div>
@@ -2261,7 +2261,7 @@ function attachSession(ws: WebSocket, sessionName: string): void {
2261
2261
  }
2262
2262
 
2263
2263
  export function printBanner(port: number): void {
2264
- console.log(`llmuxd v${DAEMON_VERSION}\n`);
2264
+ console.log(`llmux v${DAEMON_VERSION}\n`);
2265
2265
  const addrs = getAddresses(port);
2266
2266
  const width = Math.max(10, ...addrs.map((a) => a.label.length + 2));
2267
2267
  for (const addr of addrs) {
@@ -2272,6 +2272,6 @@ export function printBanner(port: number): void {
2272
2272
  console.log(`\n ✓ auth required — ${count} active token${count === 1 ? '' : 's'} (localhost bypasses)\n`);
2273
2273
  } else {
2274
2274
  console.log(`\n ⚠ running without auth — anyone on the network can attach.`);
2275
- console.log(` create a token with \`llmuxd token create\` to enable auth.\n`);
2275
+ console.log(` create a token with \`llmux token create\` to enable auth.\n`);
2276
2276
  }
2277
2277
  }