@danielishi/lmux 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Bratschke / SpockyMagicAI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,111 @@
1
+ # lmux
2
+
3
+ **lmux — Linux terminal multiplexer for Claude Code agent orchestration. The Linux peer of cmux (macOS) and wmux (Windows).**
4
+
5
+ lmux is a thin CLI wrapper around `tmux` that exposes a stable, scriptable interface for orchestrating multiple Claude Code agents in panes, splits, and workspaces. It mirrors the cmux API one-to-one so the same orchestration code, skills, and agent definitions work unchanged across all three platforms.
6
+
7
+ ## Platform ecosystem
8
+
9
+ | Platform | Tool | Backend | Install |
10
+ |---|---|---|---|
11
+ | Windows | [wmux](https://github.com/openwong2kim/wmux) | Electron + ConPTY | Installer |
12
+ | macOS | [cmux](https://github.com/manaflow-ai/cmux) | Swift / AppKit | Homebrew |
13
+ | **Linux** | **lmux** | **tmux wrapper** | **curl \| bash** |
14
+
15
+ All three expose the same CLI surface (`<x>mux send`, `read-screen`, `new-split`, `tree`, `identify`, ...), so an agent skill written against one runs against all of them.
16
+
17
+ ## Requirements
18
+
19
+ - Linux (any distribution with bash)
20
+ - `tmux` >= 3.0
21
+ - `bash` >= 4.0
22
+
23
+ ## Install
24
+
25
+ Preferred (npm):
26
+
27
+ ```bash
28
+ npm install -g lmux
29
+ ```
30
+
31
+ Alternative (curl | bash):
32
+
33
+ ```bash
34
+ curl -sL https://raw.githubusercontent.com/DanielIshi/lmux/master/install.sh | bash
35
+ ```
36
+
37
+ The installer symlinks `lmux*` into `/usr/local/bin/` and verifies the install with `lmux identify`. The npm install registers the `lmux*` binaries on your `PATH` via npm's global bin directory.
38
+
39
+ ## Quick start
40
+
41
+ ```bash
42
+ tmux new-session -d -s agents # start a tmux session
43
+ lmux new-split right # → surface:2
44
+ lmux send --surface surface:1 "claude\n" # launch claude in left pane
45
+ lmux send --surface surface:2 "claude\n" # launch claude in right pane
46
+ lmux read-screen --surface surface:1 # read left pane output
47
+ ```
48
+
49
+ ## Command reference
50
+
51
+ | Command | Flags | Description |
52
+ |---|---|---|
53
+ | `lmux send` | `--surface surface:N "text"` | Send text to pane (append `\n` for Enter) |
54
+ | `lmux send-key` | `--surface surface:N <key>` | Send key: `return`, `ctrl+c`, `ctrl+d`, etc. |
55
+ | `lmux read-screen` | `--surface surface:N [--scrollback] [--lines N]` | Read pane output |
56
+ | `lmux new-split` | `right\|down` | Split pane, returns new `surface:N` |
57
+ | `lmux close-surface` | `--surface surface:N` | Kill pane |
58
+ | `lmux tree` | `--all --json` | JSON hierarchy of all panes |
59
+ | `lmux identify` | — | Verify lmux (tmux) environment |
60
+ | `lmux list-workspaces` | — | List workspaces (tmux windows) |
61
+ | `lmux notify` | `--title T --body B` | Desktop notification |
62
+ | `lmux-send` | `--surface surface:N "text"` | Wrapper: auto-resolves surface refs |
63
+ | `lmux-read` | `--surface surface:N [--scrollback]` | Wrapper: auto-resolves surface refs |
64
+ | `lmux-send-key` | `--surface surface:N <key>` | Wrapper: auto-resolves surface refs |
65
+ | `lmux-grid` | `<rows> <cols>` | Build a rows×cols grid of panes; returns JSON array of new `surface:N` refs |
66
+
67
+ See [commands/lmux.md](commands/lmux.md) for full per-command documentation and tmux equivalents.
68
+
69
+ ## Wrapper scripts
70
+
71
+ `lmux-send`, `lmux-read`, `lmux-send-key`, and `lmux-grid` are thin convenience wrappers around the core commands. They:
72
+
73
+ - Auto-resolve surface references against the active workspace (no need to spell out the full tmux target).
74
+ - Apply newline normalization automatically.
75
+ - Are the recommended entrypoints for agent skills and orchestration loops — the bare `lmux <verb>` form is the lower-level primitive.
76
+
77
+ `lmux-grid <rows> <cols>` builds a rows×cols pane grid in the current workspace and prints a JSON array of the new `surface:N` references — convenient for spawning a swarm of agents in one call.
78
+
79
+ ## Newline rules (critical)
80
+
81
+ Same semantics as cmux — get this wrong and your agent will type its prompt without ever pressing Enter:
82
+
83
+ - **`lmux send "text"`** — sends literal text. **No Enter is pressed.**
84
+ - **`lmux send "text\n"`** — sends text *and* Enter. Use this to submit a prompt.
85
+ - **`lmux send-key return`** — presses Enter on its own (e.g. to dismiss a prompt).
86
+ - Multi-line input: embed `\n` between lines; the final `\n` submits.
87
+ - **Never** rely on a trailing space or shell behavior to submit — be explicit.
88
+
89
+ ## Claude Code integration
90
+
91
+ lmux is designed to be driven by Claude Code agents. See the `using-lmux` skill at [`skills/using-lmux/SKILL.md`](skills/using-lmux/SKILL.md) for the canonical agent-side usage patterns, including the orchestration loop, polling cadence, and cleanup contract.
92
+
93
+ For multi-agent setups (lead + worker panes, cross-server SSH panes), see [commands/lmux-team.md](commands/lmux-team.md).
94
+
95
+ ## Uninstall
96
+
97
+ ```bash
98
+ npm rm -g lmux
99
+ # or, if installed via curl|bash:
100
+ sudo rm /usr/local/bin/lmux*
101
+ ```
102
+
103
+ ## Troubleshooting
104
+
105
+ - **`lmux: command not found`** — the `lmux*` binaries are not on your `PATH`. For npm installs, check that npm's global bin directory (`npm config get prefix`/`bin`) is on `PATH`. For curl|bash installs, verify the symlinks landed in `/usr/local/bin/` and that this directory is on `PATH`.
106
+ - **`lmux: not inside a tmux session (TMUX env var unset)`** — every lmux command (except `identify` reporting the error) must run inside a tmux session. Start one first: `tmux new -s agents`, then re-run your command from inside that session.
107
+ - **`tmux >= 3.0 required`** — check your tmux version with `tmux -V`. Upgrade via your package manager (`apt install tmux`, `dnf install tmux`, `brew install tmux`). Distributions on older LTS lines (e.g. Ubuntu 18.04) ship 2.x and will not work — install from a backports repo or build from source.
108
+
109
+ ## License
110
+
111
+ MIT
package/bin/lmux ADDED
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env bash
2
+ # lmux — Linux tmux wrapper with a CLI surface compatible with cmux/wmux.
3
+ # Surface refs are formatted "surface:N" or "session:N" where N is a tmux
4
+ # pane index. The wrapper extracts N and calls tmux with `-t N`.
5
+
6
+ set -euo pipefail
7
+
8
+ _die() { echo "lmux: $*" >&2; exit 2; }
9
+
10
+ # Extract the trailing pane index from a surface ref like "surface:3" or "main:3".
11
+ _surface_index() {
12
+ local ref="$1"
13
+ case "$ref" in
14
+ *:*) echo "${ref##*:}" ;;
15
+ *) _die "invalid surface ref: $ref (expected surface:N)" ;;
16
+ esac
17
+ }
18
+
19
+ # Parse a flag pair "--name value" out of an arg list, echoing the value.
20
+ # Usage: val=$(_get_flag --surface "$@")
21
+ _get_flag() {
22
+ local name="$1"; shift
23
+ while [ $# -gt 0 ]; do
24
+ if [ "$1" = "$name" ]; then
25
+ echo "$2"
26
+ return 0
27
+ fi
28
+ shift
29
+ done
30
+ return 1
31
+ }
32
+
33
+ cmd_send() {
34
+ local surface text
35
+ surface=$(_get_flag --surface "$@") || _die "send: missing --surface"
36
+ # Last positional arg is the text payload.
37
+ text=""
38
+ while [ $# -gt 0 ]; do
39
+ case "$1" in
40
+ --surface) shift 2 ;;
41
+ *) text="$1"; shift ;;
42
+ esac
43
+ done
44
+ [ -z "$text" ] && _die "send: missing text"
45
+ local idx
46
+ idx=$(_surface_index "$surface")
47
+
48
+ if [[ "$text" == *'\n' ]]; then
49
+ local body="${text%\\n}"
50
+ tmux send-keys -t "$idx" "$body" Enter
51
+ else
52
+ tmux send-keys -t "$idx" "$text"
53
+ fi
54
+ }
55
+
56
+ cmd_send_key() {
57
+ local surface key
58
+ surface=$(_get_flag --surface "$@") || _die "send-key: missing --surface"
59
+ key=""
60
+ while [ $# -gt 0 ]; do
61
+ case "$1" in
62
+ --surface) shift 2 ;;
63
+ *) key="$1"; shift ;;
64
+ esac
65
+ done
66
+ [ -z "$key" ] && _die "send-key: missing key name"
67
+ local idx
68
+ idx=$(_surface_index "$surface")
69
+
70
+ local mapped
71
+ case "$key" in
72
+ return|enter) mapped="Enter" ;;
73
+ escape|esc) mapped="Escape" ;;
74
+ tab) mapped="Tab" ;;
75
+ space) mapped="Space" ;;
76
+ backspace|bs) mapped="BSpace" ;;
77
+ up) mapped="Up" ;;
78
+ down) mapped="Down" ;;
79
+ left) mapped="Left" ;;
80
+ right) mapped="Right" ;;
81
+ ctrl+*) mapped="C-${key#ctrl+}" ;;
82
+ alt+*) mapped="M-${key#alt+}" ;;
83
+ *) _die "send-key: unknown key '$key'" ;;
84
+ esac
85
+
86
+ tmux send-keys -t "$idx" "$mapped"
87
+ }
88
+
89
+ cmd_read_screen() {
90
+ local surface scrollback=0 lines=""
91
+ surface=$(_get_flag --surface "$@") || _die "read-screen: missing --surface"
92
+ while [ $# -gt 0 ]; do
93
+ case "$1" in
94
+ --surface) shift 2 ;;
95
+ --scrollback) scrollback=1; shift ;;
96
+ --lines) lines="$2"; shift 2 ;;
97
+ *) shift ;;
98
+ esac
99
+ done
100
+ local idx
101
+ idx=$(_surface_index "$surface")
102
+
103
+ if [ -n "$lines" ]; then
104
+ tmux capture-pane -t "$idx" -p -S "-${lines}"
105
+ elif [ "$scrollback" = "1" ]; then
106
+ tmux capture-pane -t "$idx" -p -S -
107
+ else
108
+ tmux capture-pane -t "$idx" -p
109
+ fi
110
+ }
111
+
112
+ cmd_new_split() {
113
+ local dir="${1:-}"
114
+ local flag
115
+ case "$dir" in
116
+ right) flag="-h" ;;
117
+ down) flag="-v" ;;
118
+ *) _die "new-split: direction must be 'right' or 'down'" ;;
119
+ esac
120
+ tmux split-window "$flag" -P -F "surface:#{pane_index}"
121
+ }
122
+
123
+ cmd_close_surface() {
124
+ local surface
125
+ surface=$(_get_flag --surface "$@") || _die "close-surface: missing --surface"
126
+ local idx
127
+ idx=$(_surface_index "$surface")
128
+ tmux kill-pane -t "$idx"
129
+ }
130
+
131
+ cmd_tree() {
132
+ # Flags ignored beyond --all --json; the only supported mode.
133
+ local fmt="#{session_name}:#{window_index}:#{pane_index}:#{pane_title}"
134
+ if command -v jq >/dev/null 2>&1; then
135
+ tmux list-panes -a -F "$fmt" \
136
+ | jq -R 'select(length > 0) | split(":") | {session: .[0], window: (.[1]|tonumber), pane: (.[2]|tonumber), surface: ("surface:" + .[2]), title: (.[3] // "")}' \
137
+ | jq -s .
138
+ elif command -v python3 >/dev/null 2>&1; then
139
+ tmux list-panes -a -F "$fmt" \
140
+ | python3 -c "import sys,json; rows=[l.split(':',3) for l in sys.stdin.read().splitlines() if l]; print(json.dumps([{'session':r[0],'window':int(r[1]),'pane':int(r[2]),'surface':f'surface:{r[2]}','title':r[3] if len(r)>3 else ''} for r in rows]))"
141
+ else
142
+ echo "lmux tree: warning — neither jq nor python3 found, falling back to naive JSON escape (titles with special chars may break)" >&2
143
+ local raw
144
+ raw=$(tmux list-panes -a -F "$fmt")
145
+ printf '['
146
+ local first=1
147
+ while IFS= read -r line; do
148
+ [ -z "$line" ] && continue
149
+ IFS=':' read -r session window pane title <<<"$line"
150
+ [ "$first" = "1" ] || printf ','
151
+ first=0
152
+ local esc_title
153
+ esc_title=${title//\\/\\\\}
154
+ esc_title=${esc_title//\"/\\\"}
155
+ printf '{"session":"%s","window":%s,"pane":%s,"surface":"surface:%s","title":"%s"}' \
156
+ "$session" "$window" "$pane" "$pane" "$esc_title"
157
+ done <<<"$raw"
158
+ printf ']\n'
159
+ fi
160
+ }
161
+
162
+ cmd_identify() {
163
+ if [ -n "${TMUX:-}" ]; then
164
+ echo "tmux: ${TMUX}"
165
+ exit 0
166
+ else
167
+ echo "lmux: not inside a tmux session (TMUX env var unset)" >&2
168
+ exit 1
169
+ fi
170
+ }
171
+
172
+ cmd_list_workspaces() {
173
+ tmux list-windows -F "workspace:#{window_index} #{window_name}"
174
+ }
175
+
176
+ cmd_notify() {
177
+ local title="" body=""
178
+ while [ $# -gt 0 ]; do
179
+ case "$1" in
180
+ --title) title="$2"; shift 2 ;;
181
+ --body) body="$2"; shift 2 ;;
182
+ *) shift ;;
183
+ esac
184
+ done
185
+ if command -v notify-send >/dev/null 2>&1; then
186
+ notify-send "$title" "$body"
187
+ else
188
+ echo "[lmux notify] $title: $body"
189
+ fi
190
+ }
191
+
192
+ main() {
193
+ local sub="${1:-}"
194
+ shift || true
195
+ case "$sub" in
196
+ send) cmd_send "$@" ;;
197
+ send-key) cmd_send_key "$@" ;;
198
+ read-screen) cmd_read_screen "$@" ;;
199
+ new-split) cmd_new_split "$@" ;;
200
+ close-surface) cmd_close_surface "$@" ;;
201
+ tree) cmd_tree "$@" ;;
202
+ identify) cmd_identify "$@" ;;
203
+ list-workspaces) cmd_list_workspaces "$@" ;;
204
+ notify) cmd_notify "$@" ;;
205
+ ""|-h|--help)
206
+ cat <<EOF
207
+ lmux — Linux tmux wrapper (cmux/wmux-compatible CLI)
208
+
209
+ Commands:
210
+ send --surface S:N "text\\n"
211
+ send-key --surface S:N <return|escape|tab|ctrl+c|...>
212
+ read-screen --surface S:N [--scrollback | --lines N]
213
+ new-split <right|down>
214
+ close-surface --surface S:N
215
+ tree --all --json
216
+ identify
217
+ list-workspaces
218
+ notify --title T --body B
219
+ EOF
220
+ ;;
221
+ *) _die "unknown subcommand: $sub" ;;
222
+ esac
223
+ }
224
+
225
+ main "$@"
package/bin/lmux-grid ADDED
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env bash
2
+ # Wrapper: lmux-grid <rows> <cols>
3
+ # Creates an N-row x M-col grid of panes via repeated splits and emits a JSON
4
+ # array of the resulting surface refs (the new panes only, current pane first).
5
+ set -euo pipefail
6
+
7
+ if [ $# -ne 2 ]; then
8
+ echo "usage: lmux-grid <rows> <cols>" >&2
9
+ exit 2
10
+ fi
11
+
12
+ rows="$1"; cols="$2"
13
+ total=$((rows * cols))
14
+ if [ "$total" -lt 1 ]; then
15
+ echo "[]"
16
+ exit 0
17
+ fi
18
+
19
+ surfaces=()
20
+ # We need total-1 new splits to reach total panes from the current one.
21
+ # Strategy: alternate vertical (down) and horizontal (right) splits.
22
+ n=$((total - 1))
23
+ i=0
24
+ while [ "$i" -lt "$n" ]; do
25
+ if [ $((i % 2)) -eq 0 ]; then
26
+ s=$(lmux new-split right)
27
+ else
28
+ s=$(lmux new-split down)
29
+ fi
30
+ surfaces+=("$s")
31
+ i=$((i + 1))
32
+ done
33
+
34
+ # Apply tiled layout for evenness (best-effort; mock tmux ignores).
35
+ tmux select-layout tiled >/dev/null 2>&1 || true
36
+
37
+ # Emit JSON.
38
+ printf '['
39
+ first=1
40
+ for s in "${surfaces[@]}"; do
41
+ [ "$first" = "1" ] || printf ','
42
+ first=0
43
+ printf '"%s"' "$s"
44
+ done
45
+ printf ']\n'
package/bin/lmux-read ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ # Wrapper: lmux-read <surface:N> [--scrollback | --lines N]
3
+ set -euo pipefail
4
+ if [ $# -lt 1 ]; then
5
+ echo "usage: lmux-read <surface:N> [--scrollback | --lines N]" >&2
6
+ exit 2
7
+ fi
8
+ surface="$1"; shift
9
+ exec lmux read-screen --surface "$surface" "$@"
package/bin/lmux-send ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+ # Wrapper: lmux-send <surface:N> <text>
3
+ # Mirrors cmux-send. Just forwards to `lmux send --surface S T`.
4
+ set -euo pipefail
5
+ if [ $# -lt 2 ]; then
6
+ echo "usage: lmux-send <surface:N> <text>" >&2
7
+ exit 2
8
+ fi
9
+ surface="$1"; shift
10
+ exec lmux send --surface "$surface" "$*"
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ # Wrapper: lmux-send-key <surface:N> <key>
3
+ set -euo pipefail
4
+ if [ $# -lt 2 ]; then
5
+ echo "usage: lmux-send-key <surface:N> <key>" >&2
6
+ exit 2
7
+ fi
8
+ surface="$1"; key="$2"
9
+ exec lmux send-key --surface "$surface" "$key"
@@ -0,0 +1,123 @@
1
+ # `lmux-team` — Multi-agent orchestration with lmux
2
+
3
+ This guide shows how to drive multiple Claude Code agents from a single lead process using lmux. It assumes you've read [`lmux.md`](lmux.md) and understand surfaces, newline rules, and the wrapper scripts.
4
+
5
+ ## Creating a 2-agent session
6
+
7
+ The canonical "lead + worker" layout: a left pane running the orchestrator, a right pane running a worker agent.
8
+
9
+ ```bash
10
+ # 1. Start a dedicated tmux session (detached, so we can drive it from outside)
11
+ tmux new-session -d -s agents -x 200 -y 50
12
+
13
+ # 2. Confirm the root pane
14
+ lmux identify
15
+ # → session=agents, window=0, surface=surface:1
16
+
17
+ # 3. Split right to create the worker pane
18
+ WORKER=$(lmux new-split right)
19
+ # → surface:2
20
+
21
+ # 4. Launch claude in each pane
22
+ lmux send --surface surface:1 "claude\n"
23
+ lmux send --surface "$WORKER" "claude\n"
24
+
25
+ # 5. Wait for both to settle, then dispatch
26
+ sleep 3
27
+ lmux send --surface "$WORKER" "Read README.md and summarize it in 3 bullets.\n"
28
+ ```
29
+
30
+ The lead pane is now free to receive its own prompts, monitor the worker, or spawn more splits.
31
+
32
+ ## The orchestration loop
33
+
34
+ This is the pattern you'll use 90% of the time: dispatch a task, poll for completion, collect the result, clean up.
35
+
36
+ ```bash
37
+ dispatch() {
38
+ local surface="$1" prompt="$2"
39
+ lmux send --surface "$surface" "$prompt\n"
40
+ }
41
+
42
+ wait_for_idle() {
43
+ # Poll until the pane stops changing for `stable_for` seconds,
44
+ # or until `timeout` seconds elapse.
45
+ local surface="$1" timeout="${2:-300}" stable_for="${3:-5}"
46
+ local last="" now="" stable_since=$SECONDS deadline=$((SECONDS + timeout))
47
+
48
+ while [ $SECONDS -lt $deadline ]; do
49
+ now=$(lmux read-screen --surface "$surface" --lines 40)
50
+ if [ "$now" = "$last" ]; then
51
+ if [ $((SECONDS - stable_since)) -ge "$stable_for" ]; then
52
+ return 0
53
+ fi
54
+ else
55
+ last="$now"
56
+ stable_since=$SECONDS
57
+ fi
58
+ sleep 1
59
+ done
60
+ return 1 # timed out
61
+ }
62
+
63
+ collect() {
64
+ local surface="$1"
65
+ lmux read-screen --surface "$surface" --scrollback
66
+ }
67
+
68
+ cleanup() {
69
+ local surface="$1"
70
+ lmux send-key --surface "$surface" ctrl+c # interrupt
71
+ sleep 1
72
+ lmux send --surface "$surface" "/exit\n" # graceful claude exit
73
+ sleep 1
74
+ lmux close-surface --surface "$surface"
75
+ }
76
+
77
+ # Usage
78
+ WORKER=$(lmux new-split right)
79
+ lmux send --surface "$WORKER" "claude\n"
80
+ sleep 3
81
+ dispatch "$WORKER" "Summarize README.md in 3 bullets."
82
+ wait_for_idle "$WORKER" 180 5 || echo "Worker timed out"
83
+ RESULT=$(collect "$WORKER")
84
+ cleanup "$WORKER"
85
+ echo "$RESULT"
86
+ ```
87
+
88
+ ### Tuning the loop
89
+
90
+ - **`stable_for`** — raise it for agents that pause mid-thought (3–10 s typical).
91
+ - **`timeout`** — set generously; a Claude task that exceeds it is almost always stuck, not slow.
92
+ - **Polling cadence** — 1 s is the sweet spot. Faster wastes CPU on `capture-pane`; slower delays the next dispatch.
93
+ - **Idempotency** — always design tasks so re-dispatching is safe; transient network blips happen.
94
+
95
+ ## Cross-server pattern (SSH pane + lmux)
96
+
97
+ To drive an agent on another machine, dedicate a pane to an SSH session and treat it like any other surface.
98
+
99
+ ```bash
100
+ REMOTE=$(lmux new-split down)
101
+ lmux send --surface "$REMOTE" "ssh user@host\n"
102
+ sleep 2 # wait for login
103
+ lmux send --surface "$REMOTE" "cd /srv/app && claude\n"
104
+ sleep 3
105
+ lmux send --surface "$REMOTE" "Show me the last 20 log lines.\n"
106
+ ```
107
+
108
+ Caveats:
109
+
110
+ - **Authentication** — use key-based SSH; don't expect the orchestrator to type passwords.
111
+ - **Liveness** — keepalives (`ServerAliveInterval 30` in `~/.ssh/config`) prevent silent disconnects from breaking the polling loop.
112
+ - **Detection** — `lmux read-screen` shows whatever the remote terminal renders. If the remote shell prompt looks identical to the local one, distinguish them with a unique `PS1` over SSH.
113
+ - **Cleanup** — exit the remote shell *before* closing the surface to avoid orphaned remote processes.
114
+
115
+ ## Integration with the agent-mux skill
116
+
117
+ The `agent-mux` skill (shared across wmux/cmux/lmux) wraps this entire pattern in a stable, declarative interface:
118
+
119
+ - It picks the right backend (`wmux` / `cmux` / `lmux`) based on the host OS — your skill code doesn't change.
120
+ - It provides ready-made helpers for dispatch / wait / collect / cleanup with sane defaults.
121
+ - It coordinates multiple worker surfaces (fan-out / fan-in) and aggregates results.
122
+
123
+ See the `using-lmux` skill (`skills/using-lmux/SKILL.md`) for the Linux-specific notes, and the `agent-mux` skill for the cross-platform orchestration API. When writing a new orchestration skill, prefer `agent-mux` over driving `lmux` directly — it's what guarantees your skill works unchanged on macOS and Windows too.
@@ -0,0 +1,183 @@
1
+ # `lmux` command reference
2
+
3
+ `lmux` is a tmux-backed CLI for orchestrating Claude Code agents on Linux. It is API-compatible with `cmux` (macOS) and `wmux` (Windows) — the same flags, the same surface model, the same newline rules.
4
+
5
+ ## Concepts
6
+
7
+ ### Surface references
8
+
9
+ Every pane is addressed as `surface:N`, where `N` is a stable integer assigned by lmux when the pane is created. Surfaces are scoped to the active tmux session and survive splits, layout changes, and zoom toggles.
10
+
11
+ - The first/root pane of a session is `surface:1`.
12
+ - `lmux new-split` returns the new surface ID on stdout.
13
+ - `lmux tree --json` lists all surfaces with their parents, sizes, and TTY paths.
14
+
15
+ ### Control key sequences
16
+
17
+ `lmux send-key` accepts symbolic names. The most common:
18
+
19
+ | Name | Effect |
20
+ |---|---|
21
+ | `return` / `enter` | Press Enter |
22
+ | `tab` | Press Tab |
23
+ | `escape` / `esc` | Press Escape |
24
+ | `space` | Press Space |
25
+ | `backspace` | Press Backspace |
26
+ | `up` / `down` / `left` / `right` | Arrow keys |
27
+ | `ctrl+c` | Interrupt (SIGINT) |
28
+ | `ctrl+d` | EOF / exit |
29
+ | `ctrl+l` | Clear screen |
30
+ | `ctrl+z` | Suspend |
31
+ | `pageup` / `pagedown` | Page navigation |
32
+
33
+ Compound modifiers use `+`: `ctrl+a`, `alt+f`, `shift+tab`.
34
+
35
+ ### Newline handling
36
+
37
+ This is the single most common source of "agent hung" bugs. Internalize it.
38
+
39
+ - `lmux send "hello"` — types `hello`. **No Enter.**
40
+ - `lmux send "hello\n"` — types `hello` *and* presses Enter.
41
+ - `lmux send-key return` — presses Enter without typing anything.
42
+ - `lmux send "line1\nline2\n"` — types two lines and submits.
43
+
44
+ The wrappers (`lmux-send`, `lmux-read`, `lmux-send-key`) follow the same rules.
45
+
46
+ ---
47
+
48
+ ## Commands
49
+
50
+ ### `lmux send`
51
+
52
+ Send literal text to a pane.
53
+
54
+ ```
55
+ lmux send --surface surface:N "text"
56
+ ```
57
+
58
+ - **Flags:**
59
+ - `--surface surface:N` (required) — target pane
60
+ - `"text"` (positional) — payload; `\n` is interpreted as Enter
61
+ - **Example:**
62
+ ```
63
+ lmux send --surface surface:2 "echo hello\n"
64
+ ```
65
+ - **tmux equivalent:** `tmux send-keys -t <target> "echo hello" Enter`
66
+
67
+ ### `lmux send-key`
68
+
69
+ Press a named key or key combo.
70
+
71
+ ```
72
+ lmux send-key --surface surface:N <key>
73
+ ```
74
+
75
+ - **Flags:**
76
+ - `--surface surface:N` (required)
77
+ - `<key>` (positional) — see Control key sequences above
78
+ - **Example:**
79
+ ```
80
+ lmux send-key --surface surface:1 ctrl+c
81
+ ```
82
+ - **tmux equivalent:** `tmux send-keys -t <target> C-c`
83
+
84
+ ### `lmux read-screen`
85
+
86
+ Read the current visible pane buffer.
87
+
88
+ ```
89
+ lmux read-screen --surface surface:N [--scrollback] [--lines N]
90
+ ```
91
+
92
+ - **Flags:**
93
+ - `--surface surface:N` (required)
94
+ - `--scrollback` — include history above the visible region
95
+ - `--lines N` — limit output to the last N lines
96
+ - **Example:**
97
+ ```
98
+ lmux read-screen --surface surface:2 --lines 50
99
+ ```
100
+ - **tmux equivalent:** `tmux capture-pane -p -t <target> [-S -]`
101
+
102
+ ### `lmux new-split`
103
+
104
+ Split the current (or specified) pane and return the new surface ID.
105
+
106
+ ```
107
+ lmux new-split right|down [--surface surface:N]
108
+ ```
109
+
110
+ - **Args:**
111
+ - `right` — vertical split (new pane on the right)
112
+ - `down` — horizontal split (new pane below)
113
+ - **Output:** the new `surface:N` on stdout
114
+ - **Example:**
115
+ ```
116
+ NEW=$(lmux new-split right)
117
+ lmux send --surface "$NEW" "claude\n"
118
+ ```
119
+ - **tmux equivalent:** `tmux split-window -h` / `-v`
120
+
121
+ ### `lmux close-surface`
122
+
123
+ Kill a pane.
124
+
125
+ ```
126
+ lmux close-surface --surface surface:N
127
+ ```
128
+
129
+ - **tmux equivalent:** `tmux kill-pane -t <target>`
130
+
131
+ ### `lmux tree`
132
+
133
+ Dump the pane hierarchy.
134
+
135
+ ```
136
+ lmux tree [--all] [--json]
137
+ ```
138
+
139
+ - **Flags:**
140
+ - `--all` — include every workspace (tmux window), not just the active one
141
+ - `--json` — emit machine-readable JSON
142
+ - **Example:**
143
+ ```
144
+ lmux tree --all --json | jq '.workspaces[].surfaces[].id'
145
+ ```
146
+
147
+ ### `lmux identify`
148
+
149
+ Verify lmux is functional in the current environment. Prints the lmux version, tmux version, active session/window/pane, and exits non-zero if anything is off.
150
+
151
+ ```
152
+ lmux identify
153
+ ```
154
+
155
+ ### `lmux list-workspaces`
156
+
157
+ List workspaces (tmux windows) in the active session.
158
+
159
+ ```
160
+ lmux list-workspaces
161
+ ```
162
+
163
+ - **tmux equivalent:** `tmux list-windows`
164
+
165
+ ### `lmux notify`
166
+
167
+ Fire a desktop notification (best-effort; uses `notify-send` if available).
168
+
169
+ ```
170
+ lmux notify --title "Build done" --body "All tests passed"
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Wrapper scripts
176
+
177
+ `lmux-send`, `lmux-read`, `lmux-send-key` are the recommended entrypoints for agents. They mirror the bare verbs but:
178
+
179
+ - Auto-resolve `surface:N` against the active workspace (no need to pass session/window).
180
+ - Normalize newlines.
181
+ - Emit the same exit codes as the underlying commands.
182
+
183
+ Use the wrappers in skills and orchestration loops; use the bare verbs only when you need precise control over a non-active workspace.
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@danielishi/lmux",
3
+ "version": "0.1.1",
4
+ "description": "Linux terminal multiplexer companion for Claude Code agents — drop-in API equivalent of cmux (macOS) and wmux (Windows)",
5
+ "bin": {
6
+ "lmux": "./bin/lmux",
7
+ "lmux-send": "./bin/lmux-send",
8
+ "lmux-read": "./bin/lmux-read",
9
+ "lmux-send-key": "./bin/lmux-send-key",
10
+ "lmux-grid": "./bin/lmux-grid"
11
+ },
12
+ "scripts": {
13
+ "postinstall": "node -e \"const { execSync } = require('child_process'); try { execSync('tmux -V', { stdio: 'ignore' }); } catch { console.warn('[lmux] WARNING: tmux not found on PATH. Install it (apt/dnf/brew install tmux) for lmux to work.'); }\"",
14
+ "test": "echo 'Tests use bats — run: bats tests/'"
15
+ },
16
+ "files": ["bin/", "README.md", "LICENSE", "commands/", "skills/"],
17
+ "os": ["linux", "darwin"],
18
+ "engines": { "node": ">=14" },
19
+ "keywords": ["claude-code", "tmux", "ai-agents", "terminal-multiplexer", "cmux", "wmux", "agent-orchestration"],
20
+ "author": "Daniel Bratschke",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/DanielIshi/lmux.git"
25
+ },
26
+ "homepage": "https://github.com/DanielIshi/lmux#readme",
27
+ "bugs": { "url": "https://github.com/DanielIshi/lmux/issues" }
28
+ }
@@ -0,0 +1,162 @@
1
+ ---
2
+ name: using-lmux
3
+ description: >
4
+ Use lmux to orchestrate Claude Code agents in tmux panes on Linux.
5
+ Trigger on: lmux operations, coordinating agents in tmux, Linux multi-agent
6
+ sessions, lmux send/read/split commands.
7
+ allowed-tools:
8
+ - Bash
9
+ ---
10
+
11
+ # Using lmux
12
+
13
+ `lmux` is a thin tmux wrapper that mirrors the `cmux` CLI so Claude Code can drive panes on
14
+ Linux servers exactly the same way it drives them on macOS. Use this skill whenever you need
15
+ to spin up, talk to, or read from agents running in tmux.
16
+
17
+ ## Prerequisites
18
+
19
+ - `tmux >= 3.2` installed on the host.
20
+ - `lmux` on `$PATH` (the `lmux`, `lmux-send`, `lmux-read`, `lmux-send-key` wrappers).
21
+ - Running inside a tmux session (`$TMUX` set) for surface refs to resolve.
22
+
23
+ If `$TMUX` is unset, start one first:
24
+ ```bash
25
+ lmux new-session -s agents
26
+ ```
27
+
28
+ ## Surface References
29
+
30
+ A "surface" is a tmux pane addressed as `session:window.pane` or shorthand `session:N` (pane
31
+ index inside the active window). Discover them with:
32
+
33
+ ```bash
34
+ lmux tree --all --json
35
+ ```
36
+
37
+ Returns a JSON tree of sessions → windows → panes with their surface refs and current
38
+ command. Use this to pick a target before sending input.
39
+
40
+ ## Core Commands
41
+
42
+ ### List
43
+
44
+ ```bash
45
+ lmux list-workspaces # all tmux sessions
46
+ lmux tree --all --json # full hierarchy as JSON
47
+ ```
48
+
49
+ ### Split / new pane
50
+
51
+ ```bash
52
+ lmux new-split right # split current pane to the right
53
+ lmux new-split down # split downwards
54
+ lmux new-session -s mywork # brand-new session
55
+ ```
56
+
57
+ ### Send a command
58
+
59
+ `lmux-send` writes literal text to a pane. **You must include the trailing `\n`** — without
60
+ it the shell will not execute the line.
61
+
62
+ ```bash
63
+ lmux-send --surface agents:1 "ls -la\n"
64
+ lmux-send --surface agents:2 "claude --print 'summarize ./README.md'\n"
65
+ ```
66
+
67
+ ### Read pane output
68
+
69
+ ```bash
70
+ lmux-read --surface agents:1 # current visible buffer
71
+ lmux-read --surface agents:1 --lines 200 # last 200 lines of scrollback
72
+ ```
73
+
74
+ ### Send a key
75
+
76
+ For control keys and special keys, use `lmux-send-key` (not `lmux-send`):
77
+
78
+ ```bash
79
+ lmux-send-key --surface agents:1 return
80
+ lmux-send-key --surface agents:1 c-c # Ctrl-C
81
+ lmux-send-key --surface agents:1 c-d # Ctrl-D (EOF / exit)
82
+ ```
83
+
84
+ ## Orchestration Pattern
85
+
86
+ A typical multi-agent run looks like this:
87
+
88
+ 1. **Bootstrap panes.** One `lmux new-split` per agent; label by setting the pane title:
89
+ ```bash
90
+ lmux-send --surface agents:2 $'printf "\\033]2;researcher\\007"\n'
91
+ ```
92
+ 2. **Dispatch.** Send each agent a one-shot Claude invocation wrapped in markers so output
93
+ capture is deterministic:
94
+ ```bash
95
+ lmux-send --surface agents:2 \
96
+ "echo '=== START r1 ===' && claude --print 'task A' && echo '=== END r1 ==='\n"
97
+ ```
98
+ 3. **Poll.** Loop on `lmux-read` every ~2 seconds; stop when `=== END r1 ===` appears or you
99
+ hit a timeout.
100
+ 4. **Collect.** Slice the buffer between the START/END markers, strip ANSI, hand the result
101
+ back to the orchestrator.
102
+ 5. **Aggregate.** Merge per-pane results into a single summary for the user.
103
+
104
+ ## Newline & Quoting Rules
105
+
106
+ - `lmux-send` does **not** add a newline. Always end commands with `\n`.
107
+ - Use double quotes around the payload so `\n` is interpreted by the shell.
108
+ - For payloads that contain literal `$` or backticks, prefer single quotes and embed `\n` via
109
+ `$'...\n'` (Bash ANSI-C quoting):
110
+ ```bash
111
+ lmux-send --surface agents:1 $'echo "$HOME"\n'
112
+ ```
113
+ - `lmux-send-key` takes a key name (e.g. `return`, `c-c`, `tab`, `up`), not raw text.
114
+
115
+ ## Examples
116
+
117
+ ### Two parallel researchers
118
+
119
+ ```bash
120
+ lmux new-split right
121
+ lmux new-split down
122
+ # pane indices are now agents:1 (orchestrator), agents:2, agents:3
123
+
124
+ lmux-send --surface agents:2 \
125
+ "echo '=== START a ===' && claude --print 'summarize repo X' && echo '=== END a ==='\n"
126
+ lmux-send --surface agents:3 \
127
+ "echo '=== START b ===' && claude --print 'summarize repo Y' && echo '=== END b ==='\n"
128
+
129
+ # Poll
130
+ for pane in agents:2 agents:3; do
131
+ until lmux-read --surface "$pane" | grep -q "=== END "; do sleep 2; done
132
+ done
133
+
134
+ # Collect
135
+ lmux-read --surface agents:2 --lines 500 \
136
+ | sed -n '/=== START a ===/,/=== END a ===/p'
137
+ ```
138
+
139
+ ### Cancel a runaway agent
140
+
141
+ ```bash
142
+ lmux-send-key --surface agents:2 c-c
143
+ ```
144
+
145
+ ### Tear down a session
146
+
147
+ ```bash
148
+ lmux-send-key --surface agents:1 c-d # exit shell in pane 1
149
+ # or, kill the whole session:
150
+ tmux kill-session -t agents
151
+ ```
152
+
153
+ ## Tips
154
+
155
+ - Keep one orchestrator pane that runs `claude` interactively and lets it drive the rest via
156
+ these commands. It scales much better than juggling sessions by hand.
157
+ - Always wrap fan-out work in START/END markers. Scrollback-based completion detection
158
+ without markers is fragile.
159
+ - `lmux tree --all --json` is cheap — re-run it whenever pane indices might have shifted
160
+ (e.g. after a `kill-pane`).
161
+ - For cross-server work, run lmux inside a tmux session on the remote host and drive it over
162
+ SSH; the surface refs are local to that host's tmux server.