@hmanlab/multiplayer 0.4.3
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.
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: multiplayer
|
|
3
|
+
description: Use when the user wants to start, join, or end a multiplayer session so two or more developers can collaborate in the same OpenCode session. Phase 03: companion pane auto-spawns (tmux / iTerm2 / detached terminal / manual `npx @hmanlab/multiplayer-watch`); chat roundtrip over the wire; typing indicator. Phase 02 baseline still applies: multi-peer (1 host + N guests), host handoff with 10s grace, volunteer-first successor selection, 1-hour rejoin grace for old codes. Front-load keywords: multiplayer, peer, pair, connect, join, session, host, guest, invite, code, mp-, share, collaborate, transfer, volunteer, handoff, chat, typing, companion, watch, pane, mp_host, mp_join, mp_leave, mp_cancel_leave, mp_volunteer, mp_code, mp_status, mp_rejoin, mp_chat.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# multiplayer — multi-user sessions for OpenCode
|
|
7
|
+
|
|
8
|
+
The `multiplayer-tools` plugin lets two or more `opencode` instances recognize each other and stay connected while their agents work. **Phase 03** adds the companion pane (auto-spawned into a sibling terminal region) and a chat surface — both peers see chat within 500ms on LAN, with a typing indicator while the other is composing.
|
|
9
|
+
|
|
10
|
+
The companion is a separate Node + Ink TUI process that the plugin spawns into a tmux split, an iTerm2 split, or a detached terminal window. On unrecognized terminals, the plugin prints a `npx @hmanlab/multiplayer-watch` command the user can run in any other terminal.
|
|
11
|
+
|
|
12
|
+
Intents, heartbeat/crash detection, and the Cloudflare Tunnel land in later phases.
|
|
13
|
+
|
|
14
|
+
## Roles — explicit, never auto
|
|
15
|
+
|
|
16
|
+
The plugin does **no** work at load time. Installing the plugin is safe: it adds eight tools and sits idle. The user must explicitly call a tool to enter a role.
|
|
17
|
+
|
|
18
|
+
| Role | Entered via | Bound resources |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `idle` | (default after install) | none |
|
|
21
|
+
| `host` | `mp_host` | binds local port, mints invite code |
|
|
22
|
+
| `guest` | `mp_join <code>` or `mp_rejoin <code>` | opens outbound WebSocket to host |
|
|
23
|
+
|
|
24
|
+
Only one role is active at a time. `mp_leave` returns to idle. Calling `mp_host` or `mp_join` while in the wrong role returns an error.
|
|
25
|
+
|
|
26
|
+
## Port: 7332 (one digit off from Kilo Code's 7331)
|
|
27
|
+
|
|
28
|
+
Default port is **`MP_PORT=7332`**. We deliberately avoid 7331 because the [Kilo Code](https://marketplace.visualstudio.com/items?itemName=kilocode.kilo-code) VS Code extension also binds 7331.
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
MP_PORT=8332 opencode # both peers must use the same MP_PORT
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Host address (LAN): `MP_HOST`
|
|
35
|
+
|
|
36
|
+
Default host is `localhost`. For two machines on the same WiFi, set `MP_HOST` to the host machine's LAN IP (or hostname) on the **guest's** side:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Machine A (Bob, host)
|
|
40
|
+
MP_PORT=7332 opencode
|
|
41
|
+
|
|
42
|
+
# Machine B (Carol, guest)
|
|
43
|
+
MP_PORT=7332 MP_HOST=192.168.1.42 opencode
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Tools
|
|
47
|
+
|
|
48
|
+
| Tool | Role | What it does |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `mp_host` | any → host | Bind port, mint invite code, return URL |
|
|
51
|
+
| `mp_join <code>` | any → guest | Dial host, auth, exchange hello |
|
|
52
|
+
| `mp_leave` | host or guest | Host: 10s grace + auto-transfer. Guest: close WS immediately. |
|
|
53
|
+
| `mp_cancel_leave` | host (during grace) | Abort a pending transfer |
|
|
54
|
+
| `mp_volunteer` | guest | Opt in as next-host candidate (preferred over longest-connected) |
|
|
55
|
+
| `mp_code` | any | Host: current invite code. Guest: host's handle. |
|
|
56
|
+
| `mp_status` | any | Role, port, code, peers list, host handle, leaving-state info |
|
|
57
|
+
| `mp_rejoin <code>` | any → guest | Rejoin with a grace code (valid 1 hour after the host change) |
|
|
58
|
+
| `mp_chat <text>` | host or guest | Send a chat message to all peers. Same as typing in the companion pane's input box. |
|
|
59
|
+
| `mp_watch` | any | Launch the companion TUI pane in a sibling terminal (presence, chat, input box). |
|
|
60
|
+
|
|
61
|
+
`mp_history` (recent host transfers) is deferred to Phase 07 — the data is already persisted in `state.json`.
|
|
62
|
+
|
|
63
|
+
## Companion pane
|
|
64
|
+
|
|
65
|
+
After `opencode` is fully initialized, the plugin spawns a **separate Node + Ink TUI** in a sibling terminal region:
|
|
66
|
+
|
|
67
|
+
1. **tmux split** — if `$TMUX` is set and `tmux` is on `$PATH`, the current tmux pane splits horizontally.
|
|
68
|
+
2. **iTerm2 split** — on macOS when the parent terminal is iTerm2, the current session splits vertically via AppleScript.
|
|
69
|
+
3. **Detached terminal window** — opens a new window in Terminal.app, Windows Terminal, `gnome-terminal`, `konsole`, `xfce4-terminal`, `kitty`, `wezterm`, `alacritty`, or `ghostty`.
|
|
70
|
+
4. **Manual fallback** — on any other terminal, the plugin emits a toast with the command to run:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx @hmanlab/multiplayer-watch
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The `npx` command connects to the plugin's Unix-domain socket and renders the same UI in any other terminal.
|
|
77
|
+
|
|
78
|
+
The companion shows: presence list (left), chat history (right, scrollable), input box (bottom). The plugin spawns asynchronously after the prompt is up — startup overhead is ≤ 50ms.
|
|
79
|
+
|
|
80
|
+
To disable the companion entirely (e.g. for headless setups), set `MP_NO_COMPANION=1` before launching `opencode`.
|
|
81
|
+
|
|
82
|
+
## Chat
|
|
83
|
+
|
|
84
|
+
Chat messages are plain text typed into the companion's input box. They flow over the same WebSocket the host already uses for signaling — no new port, no new dependency.
|
|
85
|
+
|
|
86
|
+
- **Sender**: types in companion → plugin sends `chat` over the wire → host fans out to other peers.
|
|
87
|
+
- **Recipient**: receives `chat` from wire → companion shows it in the chat history (with the sender's handle and timestamp).
|
|
88
|
+
- **Typing indicator**: when a peer focuses their input, the companion sends `typing:start`; on blur, `typing:stop`. The plugin forwards these to other peers. The recipient sees "X is typing…" in their companion header within 200ms.
|
|
89
|
+
- **`/mp_chat <text>`** in the OpenCode prompt is the same path as the companion's input box. Use it when keyboard-only and the companion isn't visible.
|
|
90
|
+
|
|
91
|
+
Chat is **not** persisted to `state.json` — it lives in memory only. The history cap is 500 messages, scrollable in the companion.
|
|
92
|
+
|
|
93
|
+
## Handle resolution (`MP_HANDLE`)
|
|
94
|
+
|
|
95
|
+
Resolution order: `MP_HANDLE` env var → `~/.hl-plugins/multiplayer/handle` → `$USER`.
|
|
96
|
+
|
|
97
|
+
Validation: lowercase, `[a-z0-9-]{1,16}`.
|
|
98
|
+
|
|
99
|
+
**Collision suffix**: when two peers join with the same handle, the host assigns the second peer a unique suffix (e.g. `alice-7k2`) and tells them via the `welcome` message.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
MP_HANDLE=alice opencode # your invite code will be mp-alice-...
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Slash command
|
|
106
|
+
|
|
107
|
+
OpenCode does not have a built-in `/mp` slash command yet. The user types intent in plain English and the LLM maps it to the right tool. Common phrasings:
|
|
108
|
+
|
|
109
|
+
| User says | Tool called |
|
|
110
|
+
|---|---|
|
|
111
|
+
| "host a multiplayer session" / "start a session" | `mp_host` |
|
|
112
|
+
| "join `mp-...`" / "connect to `mp-...`" | `mp_join(code=...)` |
|
|
113
|
+
| "leave" / "end session" / "I'm done" | `mp_leave` |
|
|
114
|
+
| "cancel the leave" / "actually stay" | `mp_cancel_leave` |
|
|
115
|
+
| "I'll be next host" / "volunteer" | `mp_volunteer` |
|
|
116
|
+
| "what's the invite code?" | `mp_code` |
|
|
117
|
+
| "what's the multiplayer status?" | `mp_status` |
|
|
118
|
+
| "rejoin `mp-...`" / "come back as guest" | `mp_rejoin(code=...)` |
|
|
119
|
+
| "open the companion" / "show the watch pane" / "launch companion" | `mp_watch` |
|
|
120
|
+
|
|
121
|
+
## Host handoff flow
|
|
122
|
+
|
|
123
|
+
1. **Host** runs `mp_leave`. All peers see `host leaving in 10s`.
|
|
124
|
+
2. Any peer can run `mp_volunteer` to opt in as next host.
|
|
125
|
+
3. After 10s, the host picks the successor:
|
|
126
|
+
- Priority 1: any volunteer (longest-connected wins ties)
|
|
127
|
+
- Priority 2: longest-connected peer
|
|
128
|
+
4. The host sends `transfer_to_me` to the successor.
|
|
129
|
+
5. The successor stops being a guest, starts a new host server, mints a fresh code, and sends `transfer_confirmed` back.
|
|
130
|
+
6. The old host broadcasts `transfer_start` to all other peers and stops.
|
|
131
|
+
7. Other peers close their old WebSocket and dial the new host with the new code.
|
|
132
|
+
8. If the new host's port is blocked, the plugin auto-cascades to the next successor. If all fail, `session_ended` is broadcast and all peers return to idle.
|
|
133
|
+
|
|
134
|
+
The new host's code is in `mp_code` after the transfer. The old host's code stays valid for 1 hour — the old host can run `mp_rejoin <old-code>` to come back as a guest.
|
|
135
|
+
|
|
136
|
+
## Rejoin grace
|
|
137
|
+
|
|
138
|
+
When a host change happens, the old code is added to the new host's grace list for 1 hour. The new host accepts both the current code and any well-formed grace code in its list.
|
|
139
|
+
|
|
140
|
+
- `mp_rejoin` with a code < 1 hour old: accepted, guest joins
|
|
141
|
+
- `mp_rejoin` with a code > 1 hour old (or not in the grace list): rejected with an error toast
|
|
142
|
+
|
|
143
|
+
## State file
|
|
144
|
+
|
|
145
|
+
Persistent state lives at `~/.hl-plugins/multiplayer/state.json` (atomic write — `.tmp` + rename). Contains:
|
|
146
|
+
|
|
147
|
+
- `myHandle` — your chosen handle
|
|
148
|
+
- `lastHostUrl` — the last host you joined
|
|
149
|
+
- `graceCodes` — `[{ code, handle, validUntil }]` codes the new host accepts
|
|
150
|
+
- `history` — recent events (host_started, host_changed, session_ended, guest_joined, guest_left)
|
|
151
|
+
|
|
152
|
+
The chosen handle is also persisted to `~/.hl-plugins/multiplayer/handle`.
|
|
153
|
+
|
|
154
|
+
## Failure modes to surface
|
|
155
|
+
|
|
156
|
+
- `Port <N> is already in use` — restart with `MP_PORT=<other>`
|
|
157
|
+
- `No host responded at ws://<host>:<port>` — host not running, or `MP_HOST`/`MP_PORT` mismatch
|
|
158
|
+
- `Join failed: invalid_code` — malformed code
|
|
159
|
+
- `rejoin failed: grace expired` — the 1-hour window has passed
|
|
160
|
+
- `session ended: no_reachable_successor` — the cascade exhausted; session is over
|
|
161
|
+
|
|
162
|
+
## When NOT to use these tools
|
|
163
|
+
|
|
164
|
+
- For real-time co-editing of the same file — use VS Code Live Share
|
|
165
|
+
- For two machines on different networks — Cloudflare Tunnel lands in Phase 05
|
|
166
|
+
- For more than ~5 peers — the protocol works but toasts get noisy
|
|
167
|
+
|
|
168
|
+
## Notes for the LLM
|
|
169
|
+
|
|
170
|
+
- The plugin load is a no-op. No toasts, no port binding, no async work.
|
|
171
|
+
- `MP_PORT` must match between host and guest. `MP_HOST` must point to the host machine on the guest's side.
|
|
172
|
+
- The host's signaling server terminates when the host's opencode exits (or `mp_leave` after the transfer).
|
|
173
|
+
- Real WebRTC is deferred. The WebSocket is used as both signaling and data channel.
|
|
174
|
+
- Grace codes expire 1 hour after the host change that retired them. After that, they are pruned from `state.json` and rejected by the host.
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hmanlab/multiplayer",
|
|
3
|
+
"version": "0.4.3",
|
|
4
|
+
"description": "Real-time multiplayer for OpenCode. Two or more developers collaborate in the same session. Phase 03: companion pane auto-spawns (tmux / iTerm2 / detached terminal); chat roundtrip; typing indicator.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"opencode",
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"prepublishOnly": "bun build ./opencode/plugin/multiplayer-tools.ts --outfile=dist/multiplayer-tools.js --target=bun --external @opencode-ai/plugin"
|
|
12
|
+
},
|
|
13
|
+
"hl-plugins": {
|
|
14
|
+
"opencodePlugin": "./dist/multiplayer-tools.js",
|
|
15
|
+
"opencodeSkill": "./opencode/skill/multiplayer/SKILL.md",
|
|
16
|
+
"defaultInstall": true
|
|
17
|
+
}
|
|
18
|
+
}
|