libtmux-mcp 0.1.0.alpha.2 → 0.1.0.alpha.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.
- checksums.yaml +4 -4
- data/README.md +342 -114
- data/lib/libtmux/mcp/application.rb +7 -3
- data/lib/libtmux/mcp/catalog.rb +3 -2
- data/lib/libtmux/mcp/observation.rb +124 -19
- data/lib/libtmux/mcp/process_identity.rb +3 -3
- data/lib/libtmux/mcp/version.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af3b345cc9efd2ed4fcb1c384d8ee9a0e8f14b0da9052eedafb5e88b285a2501
|
|
4
|
+
data.tar.gz: f6c4f2647b58a02730b17ab57190f7cfc5ef2927c923961608d2a192d9f1b077
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 62dddd73fc6080af123ef9b4b5aeb5dca20c72d8798232d69df61777f3e1aaa9b44db30d1853949e2e5d266d063b6827ed2b3f4feb4950072418d2283739bba2
|
|
7
|
+
data.tar.gz: 37bffa621af62f041a08dffa029d2f060874686983982e70a1906343fd2755bfc6b81daefeb1e2914f99a53a38ba7d88c1b90edb1982f3f48349396bdb1a91e9
|
data/README.md
CHANGED
|
@@ -12,27 +12,101 @@
|
|
|
12
12
|
# libtmux-mcp
|
|
13
13
|
|
|
14
14
|
Expose an existing tmux server over MCP stdio. Read snapshots, capture pane
|
|
15
|
-
output
|
|
16
|
-
|
|
15
|
+
output and wait for events, or enable creation, input and shell commands one
|
|
16
|
+
tool at a time.
|
|
17
|
+
|
|
18
|
+
[API reference](https://libtmux.org/en/ruby/latest/mcp/reference/) ·
|
|
19
|
+
[Tools](https://libtmux.org/en/ruby/latest/mcp/tools/) ·
|
|
20
|
+
[Guides](https://libtmux.org/en/ruby/latest/mcp/guides/) ·
|
|
21
|
+
[RubyGems](https://rubygems.org/gems/libtmux-mcp)
|
|
17
22
|
|
|
18
23
|
</div>
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
Part of [libtmux for Ruby](https://github.com/libtmux/libtmux-ruby#readme).
|
|
26
|
+
Built on [libtmux](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux)
|
|
27
|
+
and [libtmux-async](https://github.com/libtmux/libtmux-ruby/tree/master/gems/libtmux-async).
|
|
28
|
+
|
|
29
|
+
An MCP client starts `libtmux-mcp` and talks to it over stdin and stdout. The
|
|
30
|
+
official MCP SDK handles the protocol, and bounded Async tasks handle
|
|
31
|
+
transport.
|
|
32
|
+
|
|
33
|
+
**Contents** — [Quick start](#quick-start) ·
|
|
34
|
+
[Connect a client](#connect-a-client) ·
|
|
35
|
+
[Choose the server](#choose-which-tmux-server-it-serves) ·
|
|
36
|
+
[The tools](#the-tools) · [Read](#read-the-layout-and-the-screen) ·
|
|
37
|
+
[Create](#create-type-and-close) · [Run](#run-a-script-in-a-pane) ·
|
|
38
|
+
[Embed](#embed-it-in-ruby) · [Leaves out](#what-it-leaves-out) ·
|
|
39
|
+
[Support](#support-and-status) · [Documentation](#documentation)
|
|
25
40
|
|
|
26
|
-
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
Install the alpha and its `libtmux-mcp` executable. It needs Ruby 3.3 or newer
|
|
44
|
+
and tmux 3.3a or newer:
|
|
27
45
|
|
|
28
46
|
```console
|
|
29
47
|
$ gem install libtmux-mcp --pre
|
|
30
48
|
```
|
|
31
49
|
|
|
32
|
-
|
|
50
|
+
The server borrows a tmux server that is already running. Start one on a
|
|
51
|
+
socket of its own:
|
|
52
|
+
|
|
53
|
+
```console
|
|
54
|
+
$ tmux -L agent new-session -d -s work
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Add the server to Claude Code. Until you enable more tools, it serves
|
|
58
|
+
[`tmux_capabilities`][tool-tmux_capabilities] and
|
|
59
|
+
[`tmux_snapshot`][tool-tmux_snapshot]:
|
|
60
|
+
|
|
61
|
+
```console
|
|
62
|
+
$ claude mcp add tmux -- libtmux-mcp --socket-name agent --endpoint dev
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Run by hand, `libtmux-mcp` waits for input until EOF. `libtmux-mcp --help`
|
|
66
|
+
lists every option.
|
|
67
|
+
|
|
68
|
+
## Connect a client
|
|
69
|
+
|
|
70
|
+
### `claude mcp add`: Claude Code
|
|
71
|
+
|
|
72
|
+
The command in the quick start registers the server with Claude Code. Start the
|
|
73
|
+
tmux server first, as above.
|
|
74
|
+
|
|
75
|
+
### `codex mcp add`: Codex CLI
|
|
76
|
+
|
|
77
|
+
Codex CLI takes the same command after `--`:
|
|
78
|
+
|
|
79
|
+
```console
|
|
80
|
+
$ codex mcp add tmux -- libtmux-mcp --socket-name agent --endpoint dev
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `mcpServers`: clients that read JSON
|
|
84
|
+
|
|
85
|
+
A client that reads a JSON configuration, such as Claude Desktop's
|
|
86
|
+
`claude_desktop_config.json`, takes the same command as an `mcpServers` entry:
|
|
87
|
+
|
|
88
|
+
<!-- example: mcp_client/config -->
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"mcpServers": {
|
|
92
|
+
"tmux": {
|
|
93
|
+
"command": "libtmux-mcp",
|
|
94
|
+
"args": ["--socket-name", "agent", "--endpoint", "dev"]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
<!-- /example -->
|
|
100
|
+
|
|
101
|
+
If the client does not start servers with your shell's `PATH`, set `command` to
|
|
102
|
+
the absolute path that `command -v libtmux-mcp` prints.
|
|
103
|
+
|
|
104
|
+
## Choose which tmux server it serves
|
|
105
|
+
|
|
106
|
+
### `--socket` and `--socket-name`: pick the daemon
|
|
33
107
|
|
|
34
108
|
Set `TMUX_SOCKET` to an existing tmux socket. This command borrows that daemon
|
|
35
|
-
and serves MCP on stdin
|
|
109
|
+
and serves MCP on stdin and stdout:
|
|
36
110
|
|
|
37
111
|
```console
|
|
38
112
|
$ libtmux-mcp \
|
|
@@ -40,18 +114,49 @@ $ libtmux-mcp \
|
|
|
40
114
|
--endpoint local
|
|
41
115
|
```
|
|
42
116
|
|
|
43
|
-
Use `--socket-name NAME` instead of `--socket PATH` to select a named socket
|
|
44
|
-
|
|
45
|
-
|
|
117
|
+
Use `--socket-name NAME` instead of `--socket PATH` to select a named socket,
|
|
118
|
+
as the quick start does:
|
|
119
|
+
|
|
120
|
+
```console
|
|
121
|
+
$ libtmux-mcp \
|
|
122
|
+
--socket-name agent
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- EOF retires the clients the server owns and leaves the daemon running.
|
|
126
|
+
- `--endpoint` sets the public alias in discovery and resource URIs, and
|
|
127
|
+
defaults to `local`.
|
|
128
|
+
|
|
129
|
+
## The tools
|
|
130
|
+
|
|
131
|
+
Each tool name links to its page on libtmux.org, with the arguments, limits and
|
|
132
|
+
result schema. Repeat `--enable-tool` for each additional tool.
|
|
46
133
|
|
|
47
134
|
| Tools | Default | Purpose |
|
|
48
135
|
| --- | --- | --- |
|
|
49
|
-
| `tmux_capabilities
|
|
50
|
-
| `tmux_capture
|
|
51
|
-
| `tmux_create
|
|
52
|
-
| `tmux_run` | Disabled | Run a script in an explicitly enrolled zsh shell |
|
|
136
|
+
| [`tmux_capabilities`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_capabilities/), [`tmux_snapshot`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_snapshot/) | Enabled | Discover capabilities and query captured metadata |
|
|
137
|
+
| [`tmux_capture`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_capture/), [`tmux_wait`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_wait/) | Disabled | Capture a screen or wait for text/process exit |
|
|
138
|
+
| [`tmux_create`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_create/), [`tmux_send`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_send/), [`tmux_close`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_close/) | Disabled | Create entities, send text/keys and tear down exact targets |
|
|
139
|
+
| [`tmux_run`](https://libtmux.org/en/ruby/latest/mcp/tools/tmux_run/) | Disabled | Run a script in an explicitly enrolled zsh shell |
|
|
140
|
+
|
|
141
|
+
- Disabled tools are absent from discovery and denied on direct application
|
|
142
|
+
calls.
|
|
143
|
+
- An unknown tool name is refused when the server starts.
|
|
144
|
+
|
|
145
|
+
The [complete protocol recipe](../../examples/mcp_protocol.rb) exercises
|
|
146
|
+
discovery, snapshots, default denial, enabled mutations, cancellation and EOF
|
|
147
|
+
cleanup through actual pipes, including the installed executable.
|
|
148
|
+
|
|
149
|
+
## Read the layout and the screen
|
|
150
|
+
|
|
151
|
+
### [`tmux_snapshot`][tool-tmux_snapshot]: the layout, in pages
|
|
152
|
+
|
|
153
|
+
- Pages come from one immutable capture, and a cursor continues that capture.
|
|
154
|
+
- An expired, evicted or foreign cursor returns an error. The server never
|
|
155
|
+
substitutes a fresh listing.
|
|
53
156
|
|
|
54
|
-
|
|
157
|
+
### [`tmux_capture`][tool-tmux_capture]: a pane's screen
|
|
158
|
+
|
|
159
|
+
Enable the screen tools together:
|
|
55
160
|
|
|
56
161
|
```console
|
|
57
162
|
$ libtmux-mcp \
|
|
@@ -60,100 +165,223 @@ $ libtmux-mcp \
|
|
|
60
165
|
--enable-tool tmux_wait
|
|
61
166
|
```
|
|
62
167
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
168
|
+
- It reads a bounded screen and keeps its line endings. Bytes that are not
|
|
169
|
+
valid UTF-8 come back base64-encoded.
|
|
170
|
+
- With tracking, a call returns a cursor, and later calls return a splice
|
|
171
|
+
against that exact captured state. A cursor from another server generation
|
|
172
|
+
or from a respawned pane is refused.
|
|
173
|
+
- It refuses a pane whose effective `after-capture-pane` hook is not empty,
|
|
174
|
+
including inherited entries.
|
|
175
|
+
- On tmux 3.3a to 3.4 the hook is checked before the capture command runs, so
|
|
176
|
+
keep hook configuration stable while observing.
|
|
177
|
+
|
|
178
|
+
### [`tmux_wait`][tool-tmux_wait]: wait for text or exit
|
|
179
|
+
|
|
180
|
+
- It waits for screen text, or for the pane process to exit. Screen text
|
|
181
|
+
comes from control events on tmux 3.7 and newer, and from polling the pane
|
|
182
|
+
every 50 ms on older daemons, which take no control client. Process exit
|
|
183
|
+
uses process descriptors.
|
|
184
|
+
- Wrapped rows are joined, so text that the pane width split across rows
|
|
185
|
+
still matches.
|
|
186
|
+
- If the pane, or the tmux server holding it, ends before the text appears,
|
|
187
|
+
the call fails with `pane_died`, `delivery: observed` and the last rows it
|
|
188
|
+
read in the error's `capture`. If only the control stream ends while the
|
|
189
|
+
pane lives, such as after the client detaches, the failure is
|
|
190
|
+
`observation_lost`.
|
|
191
|
+
- A cancelled or expired wait closes its observers and never signals the pane
|
|
192
|
+
program.
|
|
193
|
+
- Process tracking needs a native identity backend: Linux peer pidfds with
|
|
194
|
+
matching PID namespaces, or Darwin kqueue. Without one, the call is refused.
|
|
195
|
+
|
|
196
|
+
### [Resource templates][mcp-resources]: the same data as URIs
|
|
197
|
+
|
|
198
|
+
[`tmux_metadata`][resource-tmux_metadata],
|
|
199
|
+
[`tmux_metadata_page`][resource-tmux_metadata_page] and
|
|
200
|
+
[`tmux_screen`][resource-tmux_screen] expose metadata pages and pane screens
|
|
201
|
+
under endpoint and generation URIs, with the same policy and limits as their
|
|
202
|
+
tools.
|
|
203
|
+
|
|
204
|
+
- The server does not advertise resource subscriptions.
|
|
205
|
+
- [Snapshots, cursors and references][mcp-snapshots-topic] has the full
|
|
206
|
+
contract.
|
|
207
|
+
|
|
208
|
+
## Create, type and close
|
|
209
|
+
|
|
210
|
+
### [`tmux_create`][tool-tmux_create]: new sessions, windows and panes
|
|
211
|
+
|
|
212
|
+
Enable the three tools together:
|
|
213
|
+
|
|
214
|
+
```console
|
|
215
|
+
$ libtmux-mcp \
|
|
216
|
+
--socket-name agent \
|
|
217
|
+
--enable-tool tmux_create \
|
|
218
|
+
--enable-tool tmux_send \
|
|
219
|
+
--enable-tool tmux_close
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
- It takes argument arrays as `argv`.
|
|
223
|
+
- Results carry the references tmux returned.
|
|
224
|
+
|
|
225
|
+
### [`tmux_send`][tool-tmux_send]: literal text or named keys
|
|
226
|
+
|
|
227
|
+
- Text and keys are separate variants.
|
|
228
|
+
- It reports `dispatch_only`: the input was dispatched, and the command has not
|
|
229
|
+
necessarily finished.
|
|
230
|
+
- After a cancellation, the effect stays unknown and is not reported as none.
|
|
231
|
+
|
|
232
|
+
### [`tmux_close`][tool-tmux_close]: an exact target
|
|
233
|
+
|
|
234
|
+
- It takes an exact session, window or pane reference, and never selects by
|
|
235
|
+
name.
|
|
236
|
+
|
|
237
|
+
## Run a script in a pane
|
|
238
|
+
|
|
239
|
+
### [`tmux_run`][tool-tmux_run]: authored commands in an enrolled zsh
|
|
240
|
+
|
|
241
|
+
[`tmux_run`][tool-tmux_run] runs a POSIX script in a pane's interactive zsh
|
|
242
|
+
5.9. It needs both the tool and an enrolled pane.
|
|
243
|
+
|
|
244
|
+
List the pane IDs of the server:
|
|
245
|
+
|
|
246
|
+
```console
|
|
247
|
+
$ tmux -L agent list-panes -a -F '#{pane_id}'
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
```text
|
|
251
|
+
%0
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Enable the tool and write a setup file for one exact pane:
|
|
255
|
+
|
|
256
|
+
```console
|
|
257
|
+
$ libtmux-mcp \
|
|
258
|
+
--socket-name agent \
|
|
259
|
+
--enable-tool tmux_run \
|
|
260
|
+
--enroll-pane %0=enroll.zsh
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Then, in that pane's zsh, run `source enroll.zsh`. A relative path resolves
|
|
264
|
+
from the directory where the server starts.
|
|
265
|
+
|
|
266
|
+
- `--enroll-pane` requires `--enable-tool tmux_run`, and each pane and each
|
|
267
|
+
file may appear once.
|
|
268
|
+
- The server writes a private file (mode `0600`) and never types into the
|
|
269
|
+
terminal. It refuses an existing file or a symlink.
|
|
270
|
+
- Setup files expire. `--enrollment-timeout SECONDS` sets the lifetime, at most
|
|
271
|
+
300.
|
|
272
|
+
- EOF retires pending enrollment and removes only the files the server wrote.
|
|
273
|
+
- A call takes an exact pane `target`, a POSIX `script`, and optional
|
|
274
|
+
`stdout_limit` and `stderr_limit` byte counts.
|
|
275
|
+
- The script runs with the shell's working directory and exported environment.
|
|
276
|
+
- Stdout and stderr come back separately, as UTF-8 or base64.
|
|
277
|
+
- A nonzero exit or a signal is a completion result. Output over its limit is
|
|
278
|
+
an error with completion unobserved, and never a truncated success.
|
|
279
|
+
- Cancelling a run does not prove that its child processes stopped.
|
|
280
|
+
|
|
281
|
+
## Embed it in Ruby
|
|
282
|
+
|
|
283
|
+
Require `libtmux/mcp`. Imports start no tmux process, scheduler or MCP server.
|
|
284
|
+
The [execution guide](../../docs/modes.md) covers result and ownership
|
|
285
|
+
boundaries.
|
|
286
|
+
|
|
287
|
+
### [`LibTmux::MCP::Application`][mcp-application]: serve from your own program
|
|
288
|
+
|
|
289
|
+
[`Application`][mcp-application] borrows an application-owned
|
|
290
|
+
[`LibTmux::Async::Server`][async-server]. Its
|
|
291
|
+
[`sdk_server`][mcp-application-sdk_server] supplies the SDK server that
|
|
292
|
+
[`StdioTransport`][mcp-stdiotransport] consumes:
|
|
293
|
+
|
|
294
|
+
<!-- example: mcp_protocol/main -->
|
|
295
|
+
```ruby
|
|
296
|
+
application = LibTmux::MCP::Application.new(server: scope.server, endpoint_name: "example")
|
|
297
|
+
sdk = application.sdk_server
|
|
298
|
+
input, client_input = IO.pipe
|
|
299
|
+
client_output, output = IO.pipe
|
|
300
|
+
transport = LibTmux::MCP::StdioTransport.new(server: sdk, parent: parent, input: input, output: output)
|
|
301
|
+
runner = parent.async { transport.run }
|
|
302
|
+
```
|
|
303
|
+
<!-- /example -->
|
|
304
|
+
|
|
305
|
+
The [complete program](../../examples/mcp_protocol.rb) runs this excerpt inside
|
|
306
|
+
an Async task with an open [`LibTmux::Async`][async] scope. It sends real
|
|
307
|
+
JSON-RPC messages through the pipes, checks the replies, and prints
|
|
308
|
+
`PASS mcp_protocol`.
|
|
309
|
+
|
|
310
|
+
### [`Application#invite_shell`][mcp-application-invite_shell]: enroll a shell from Ruby
|
|
311
|
+
|
|
312
|
+
Call `invite_shell` with a pane reference, `timeout:` and `expires_in:`, then
|
|
313
|
+
pass the invitation to [`accept_shell`][mcp-application-accept_shell].
|
|
314
|
+
|
|
315
|
+
- The invitation's `shell_arguments` make up the setup command to source in
|
|
316
|
+
that pane's zsh.
|
|
317
|
+
- `timeout:` bounds acquisition, and `expires_in:` sets the enrollment
|
|
318
|
+
lifetime.
|
|
319
|
+
- The application owns invitations and accepted connections until
|
|
320
|
+
[`close`][mcp-application-close]. Direct enrollment calls enforce tool
|
|
321
|
+
policy.
|
|
322
|
+
|
|
323
|
+
## What it leaves out
|
|
324
|
+
|
|
325
|
+
- **Tools you have not enabled.** Disabled tools are absent from discovery,
|
|
326
|
+
and a direct application call is denied. See [The tools](#the-tools).
|
|
327
|
+
- **Resource subscriptions.** The server does not advertise them. See the
|
|
328
|
+
[resource templates][mcp-resources].
|
|
329
|
+
- **Typing into your terminal.** The server writes a private setup file and
|
|
330
|
+
never types into a pane's terminal to enroll it. See
|
|
331
|
+
[`tmux_run`][tool-tmux_run].
|
|
332
|
+
- **A guaranteed stop.** Cancelling a run does not prove that its child
|
|
333
|
+
processes stopped. See [`tmux_run`][tool-tmux_run].
|
|
334
|
+
|
|
335
|
+
## Support and status
|
|
336
|
+
|
|
337
|
+
**Alpha.** APIs may change between releases.
|
|
338
|
+
|
|
339
|
+
- **Ruby 3.3 or newer.** The gem depends on `libtmux` and `libtmux-async` at
|
|
340
|
+
the same version, the `mcp` SDK 1.5, `digest` and `optparse`.
|
|
341
|
+
- **tmux 3.3a or newer.** The server refuses an older daemon when it starts,
|
|
342
|
+
before it serves any tool, and prints the reason and the remedy on stderr.
|
|
343
|
+
- **A daemon that changes while it runs.** A tool call returns an
|
|
344
|
+
`unsupported` result with `delivery: not_sent`. The result names the floor
|
|
345
|
+
and says to upgrade tmux and restart the server.
|
|
346
|
+
- **The `screen_contains` wait.** It uses control events on tmux 3.7 and
|
|
347
|
+
newer and polls the pane screen on older daemons, which take no control
|
|
348
|
+
client.
|
|
349
|
+
- **Linux, including WSL, and macOS.** The core
|
|
350
|
+
[compatibility workflow][compat] runs Ruby 3.3, 3.4 and 4.0 with tmux 3.3a
|
|
351
|
+
through 3.8 on Ubuntu 24.04. macOS is supported on a best-effort basis, and
|
|
352
|
+
CI coverage for it is highly limited.
|
|
353
|
+
|
|
354
|
+
## Documentation
|
|
355
|
+
|
|
356
|
+
| | |
|
|
357
|
+
| --- | --- |
|
|
358
|
+
| [Reference](https://libtmux.org/en/ruby/latest/mcp/reference/) | The embedding types and methods |
|
|
359
|
+
| [Tools](https://libtmux.org/en/ruby/latest/mcp/tools/) | One page per tool, with arguments, limits and result schema |
|
|
360
|
+
| [Guides][mcp-guides] | Connect a client to a private tmux server and select its tools |
|
|
361
|
+
| [Resources][mcp-resources] | Metadata pages and pane screens as URIs |
|
|
362
|
+
| [Public behavior][behavior-mcp-application] | The application, transport and command |
|
|
67
363
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
retires its observation resources without signaling the pane program. Strong
|
|
93
|
-
process tracking requires tmux 3.3a or later and a native identity backend:
|
|
94
|
-
Linux peer pidfds with matching process namespaces, or Darwin kqueue process
|
|
95
|
-
observation. Acquisition verifies the live daemon and pane before retaining
|
|
96
|
-
a cursor; unavailable evidence produces an explicit refusal. The
|
|
97
|
-
[compatibility workflow](https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml)
|
|
98
|
-
records each exact platform/version result: positive identity cases on
|
|
99
|
-
supported releases, and refusal jobs for tmux 3.0a, 3.1c, 3.2a and unpatched
|
|
100
|
-
3.3.
|
|
101
|
-
|
|
102
|
-
Resource templates expose metadata pages and pane screens under encoded
|
|
103
|
-
endpoint/generation URIs. They enforce the same policy and response limits as
|
|
104
|
-
their tools. Metadata pages preserve capture identity; screen resources
|
|
105
|
-
include interval, truncation and history-continuity metadata. Resource
|
|
106
|
-
subscriptions are not advertised.
|
|
107
|
-
|
|
108
|
-
## Create, send and close
|
|
109
|
-
|
|
110
|
-
Add `--enable-tool tmux_create`, `--enable-tool tmux_send` or
|
|
111
|
-
`--enable-tool tmux_close` to authorize those tools. Creation accepts argument
|
|
112
|
-
arrays; sending text and sending named keys are separate variants. Mutation
|
|
113
|
-
results contain delivery evidence and positively returned references.
|
|
114
|
-
`dispatch_only` input results do not claim program completion, and unknown
|
|
115
|
-
effects remain unknown after cancellation.
|
|
116
|
-
|
|
117
|
-
## Run authored commands
|
|
118
|
-
|
|
119
|
-
`tmux_run` requires separate policy and shell enrollment. Add
|
|
120
|
-
`--enable-tool tmux_run --enroll-pane %ID=FILE` for each exact pane, then
|
|
121
|
-
explicitly source the generated file in that pane's interactive zsh 5.9.
|
|
122
|
-
The CLI creates a private setup file and never types into the terminal.
|
|
123
|
-
It refuses existing files and symlinks. Invitations expire after 60 seconds;
|
|
124
|
-
`--enrollment-timeout` accepts at most 300 seconds. At most eight panes may be
|
|
125
|
-
enrolled. EOF retires pending enrollment and removes only files the CLI owns.
|
|
126
|
-
|
|
127
|
-
The tool accepts an exact pane target, a POSIX `script`, and separate
|
|
128
|
-
`stdout_limit`/`stderr_limit` byte counts. Scripts may contain at most 65,536
|
|
129
|
-
bytes. Each output defaults to 65,536 bytes and is capped at 262,144; the
|
|
130
|
-
application reserves its worst-case serialized response before authorization.
|
|
131
|
-
The helper inherits the enrolled shell's cwd and exported environment, uses
|
|
132
|
-
closed stdin, and reports separate UTF-8 or base64 outputs. Nonzero exit and
|
|
133
|
-
signal termination are completion results. Output overflow is an error with
|
|
134
|
-
completion unobserved, not silently truncated success. Shell variables,
|
|
135
|
-
functions, options and cwd changes do not persist in the interactive parent.
|
|
136
|
-
|
|
137
|
-
An idle, empty primary ZLE editor receives the request through a private socket.
|
|
138
|
-
A guarded tmux queue operation authorizes one script digest for one retained
|
|
139
|
-
server, pane process and enrollment generation. Execution may follow that
|
|
140
|
-
authorization; a later respawn does not redirect the prepared helper to its
|
|
141
|
-
replacement. Error responses retain known authorization and native completion
|
|
142
|
-
receipts. Cancellation does not prove that arbitrary descendants stopped.
|
|
143
|
-
The Linux and macOS compatibility jobs exercise enrollment and the installed
|
|
144
|
-
helper dependency closure; consult their results for the revision being used.
|
|
145
|
-
|
|
146
|
-
## Embed in Ruby
|
|
147
|
-
|
|
148
|
-
Require `libtmux/mcp`; imports start no tmux process, scheduler or MCP server.
|
|
149
|
-
`Application` borrows an application-owned `LibTmux::Async::Server`. Its
|
|
150
|
-
`sdk_server` supplies the SDK server consumed by `StdioTransport`. Both objects
|
|
151
|
-
stay on that application's reactor thread. See the
|
|
152
|
-
[execution guide](../../docs/modes.md) for result and ownership boundaries.
|
|
153
|
-
|
|
154
|
-
For shell enrollment, call `Application#invite_shell(reference, timeout:,
|
|
155
|
-
expires_in:)` and pass the returned invitation to `accept_shell`. The invitation
|
|
156
|
-
exposes an immutable `shell_arguments` array for an explicitly sourced setup
|
|
157
|
-
command and a monotonic `expires_at`. Its acquisition deadline is separate from
|
|
158
|
-
its enrollment lifetime. The application owns invitations and accepted
|
|
159
|
-
connections until `close`; direct enrollment calls enforce tool policy.
|
|
364
|
+
[async]: https://libtmux.org/en/ruby/latest/reference/libtmux-async/
|
|
365
|
+
[async-server]: https://libtmux.org/en/ruby/latest/reference/libtmux-async-server/
|
|
366
|
+
[behavior-mcp-application]: ../../docs/reference/behavior.md#mcp-application
|
|
367
|
+
[compat]: https://github.com/libtmux/libtmux-ruby/actions/workflows/compatibility.yml
|
|
368
|
+
[mcp-application]: https://libtmux.org/en/ruby/latest/mcp/reference/libtmux-mcp-application/
|
|
369
|
+
[mcp-application-accept_shell]: https://libtmux.org/en/ruby/latest/mcp/reference/libtmux-mcp-application-accept_shell/
|
|
370
|
+
[mcp-application-close]: https://libtmux.org/en/ruby/latest/mcp/reference/libtmux-mcp-application-close/
|
|
371
|
+
[mcp-application-invite_shell]: https://libtmux.org/en/ruby/latest/mcp/reference/libtmux-mcp-application-invite_shell/
|
|
372
|
+
[mcp-application-sdk_server]: https://libtmux.org/en/ruby/latest/mcp/reference/libtmux-mcp-application-sdk_server/
|
|
373
|
+
[mcp-guides]: https://libtmux.org/en/ruby/latest/mcp/guides/
|
|
374
|
+
[mcp-resources]: https://libtmux.org/en/ruby/latest/mcp/resources/
|
|
375
|
+
[mcp-snapshots-topic]: https://libtmux.org/en/ruby/latest/mcp/topics/snapshots-and-references/
|
|
376
|
+
[mcp-stdiotransport]: https://libtmux.org/en/ruby/latest/mcp/reference/libtmux-mcp-stdiotransport/
|
|
377
|
+
[resource-tmux_metadata]: https://libtmux.org/en/ruby/latest/mcp/resources/templates/tmux_metadata/
|
|
378
|
+
[resource-tmux_metadata_page]: https://libtmux.org/en/ruby/latest/mcp/resources/templates/tmux_metadata_page/
|
|
379
|
+
[resource-tmux_screen]: https://libtmux.org/en/ruby/latest/mcp/resources/templates/tmux_screen/
|
|
380
|
+
[tool-tmux_capabilities]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_capabilities/
|
|
381
|
+
[tool-tmux_capture]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_capture/
|
|
382
|
+
[tool-tmux_close]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_close/
|
|
383
|
+
[tool-tmux_create]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_create/
|
|
384
|
+
[tool-tmux_run]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_run/
|
|
385
|
+
[tool-tmux_send]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_send/
|
|
386
|
+
[tool-tmux_snapshot]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_snapshot/
|
|
387
|
+
[tool-tmux_wait]: https://libtmux.org/en/ruby/latest/mcp/tools/tmux_wait/
|
|
@@ -162,7 +162,7 @@ module LibTmux
|
|
|
162
162
|
IncompleteSnapshotError => "incomplete_snapshot", Cancelled => "cancelled",
|
|
163
163
|
DeadlineExceeded => "deadline", CapacityError => "capacity", TargetNotFoundError => "stale_target",
|
|
164
164
|
CommandError => "command_failed", UnsupportedFeatureError => "unsupported", ClosedError => "closed",
|
|
165
|
-
Observation::LostObservation => "observation_lost"}.fetch(error.class, "transport_error")
|
|
165
|
+
Observation::LostObservation => "observation_lost", Observation::PaneDied => "pane_died"}.fetch(error.class, "transport_error")
|
|
166
166
|
delivery = error.respond_to?(:delivery) ? (mutation ? mutation.delivery(error) : error.delivery) : :possibly_sent
|
|
167
167
|
delivery = :observed if result
|
|
168
168
|
effects = if name == "tmux_run"
|
|
@@ -177,10 +177,13 @@ module LibTmux
|
|
|
177
177
|
end
|
|
178
178
|
message = if delivery == :not_sent && LibTmux::Internal::TmuxVersion.refusal?(error)
|
|
179
179
|
LibTmux::Internal::TmuxVersion.hint
|
|
180
|
+
elsif error.is_a?(Observation::PaneDied)
|
|
181
|
+
"The pane, or its tmux server, ended before the awaited text appeared."
|
|
180
182
|
else
|
|
181
183
|
"The operation could not establish its requested result."
|
|
182
184
|
end
|
|
183
|
-
failure_response(code, message, delivery.to_s, effects: effects
|
|
185
|
+
failure_response(code, message, delivery.to_s, effects: effects,
|
|
186
|
+
capture: (error.capture if error.is_a?(Observation::PaneDied)))
|
|
184
187
|
end
|
|
185
188
|
|
|
186
189
|
def inspect
|
|
@@ -509,9 +512,10 @@ module LibTmux
|
|
|
509
512
|
value.freeze
|
|
510
513
|
end
|
|
511
514
|
|
|
512
|
-
def failure_response(code, message, delivery = "not_sent", effects: nil)
|
|
515
|
+
def failure_response(code, message, delivery = "not_sent", effects: nil, capture: nil)
|
|
513
516
|
details = {"code" => code, "message" => message, "delivery" => delivery}
|
|
514
517
|
details["effects"] = effects if effects
|
|
518
|
+
details["capture"] = capture if capture
|
|
515
519
|
::MCP::Tool::Response.new([{type: "text", text: message}], error: true,
|
|
516
520
|
structured_content: {"ok" => false, "error" => details})
|
|
517
521
|
end
|
data/lib/libtmux/mcp/catalog.rb
CHANGED
|
@@ -186,7 +186,8 @@ module LibTmux
|
|
|
186
186
|
{"type" => "object", "oneOf" => [object({"ok" => {"const" => true}, "data" => payload}),
|
|
187
187
|
object({"ok" => {"const" => false}, "error" => object({"code" => text,
|
|
188
188
|
"message" => text, "delivery" => {"enum" => %w[not_sent possibly_sent observed]},
|
|
189
|
-
"effects" => name == "tmux_run" ? run_effects : effects}
|
|
189
|
+
"effects" => name == "tmux_run" ? run_effects : effects}.merge(name == "tmux_wait" ? {"capture" => capture_output} : {}),
|
|
190
|
+
%w[code message delivery])})]}
|
|
190
191
|
end
|
|
191
192
|
|
|
192
193
|
def self.authorization
|
|
@@ -239,7 +240,7 @@ module LibTmux
|
|
|
239
240
|
elsif name == "tmux_capture"
|
|
240
241
|
"Read exact-pane screen/history rows preserving LF. Defaults: 200 lines, 65536 bytes, zero history lines, track=false. Limits: 1000 lines/history, 262144 bytes. UTF-8 or base64; truncation is separate from unknown history continuity. Tracked process cursors require tmux>=3.3a and a native process identity backend: Linux peer pidfds with matching PID namespaces, or Darwin kqueue process observation. Continuation returns an exact state row splice, not a live-output journal. Refuses capture after-hooks."
|
|
241
242
|
elsif name == "tmux_wait"
|
|
242
|
-
"Wait for observed literal screen text or the initial pane process exit. Deadline in seconds is capped by the application limit.
|
|
243
|
+
"Wait for observed literal screen text or the initial pane process exit. Deadline in seconds is capped by the application limit. Pane exit is observed through process descriptors. The screen_contains condition joins wrapped rows, uses control events on tmux>=3.7 and polls the pane screen on older daemons, which take no control client. A pane or tmux server that ends first fails with pane_died and the last rows read in error.capture; a lost control stream fails with observation_lost. Requires tmux>=3.3a and Linux peer pidfds with matching PID namespaces or Darwin kqueue process observation. Reports observed evidence, never remote termination caused by cancellation or an inferred process exit status."
|
|
243
244
|
elsif name == "tmux_send"
|
|
244
245
|
"Send literal UTF-8 text or named keys to one exact pane. Opt-in mutation; 64 KiB total text bytes, at most 256 keys. Reports tmux client completion and input dispatch only; never shell completion."
|
|
245
246
|
elsif name == "tmux_create"
|
|
@@ -5,8 +5,19 @@ require "libtmux/mcp/process_identity"
|
|
|
5
5
|
module LibTmux
|
|
6
6
|
module MCP
|
|
7
7
|
class Observation
|
|
8
|
+
POLL_SECONDS = 0.05
|
|
8
9
|
StaleCursor = Class.new(LibTmux::Error)
|
|
9
10
|
LostObservation = Class.new(LibTmux::Error)
|
|
11
|
+
# The pane, or the tmux server holding it, ended before the awaited text
|
|
12
|
+
# appeared. +capture+ is the last screen state read before it ended.
|
|
13
|
+
class PaneDied < LibTmux::Error
|
|
14
|
+
attr_reader :capture
|
|
15
|
+
|
|
16
|
+
def initialize(message, capture:, **details)
|
|
17
|
+
super(message, **details)
|
|
18
|
+
@capture = capture
|
|
19
|
+
end
|
|
20
|
+
end
|
|
10
21
|
module CleanupDetails
|
|
11
22
|
attr_reader :mcp_cleanup_errors
|
|
12
23
|
end
|
|
@@ -54,7 +65,8 @@ module LibTmux
|
|
|
54
65
|
{"screen" => "bounded_rows", "history_continuity" => "unknown",
|
|
55
66
|
"process_cursor" => conditional ? "conditional" : "unsupported",
|
|
56
67
|
"requirements" => ["tmux >= #{LibTmux::Internal::TmuxVersion::MINIMUM}", *(native || ["supported 64-bit Linux or Darwin process identity backend"]),
|
|
57
|
-
"live pane process", "empty effective capture hook", "stable capture-hook configuration on tmux < 3.5"
|
|
68
|
+
"live pane process", "empty effective capture hook", "stable capture-hook configuration on tmux < 3.5",
|
|
69
|
+
"screen_contains waits use control events from tmux #{LibTmux::Internal::TmuxVersion::CONTROL_MINIMUM} and poll the screen before it"],
|
|
58
70
|
"wait_conditions" => %w[screen_contains process_exit]}
|
|
59
71
|
end
|
|
60
72
|
|
|
@@ -162,7 +174,7 @@ module LibTmux
|
|
|
162
174
|
end
|
|
163
175
|
|
|
164
176
|
def wait
|
|
165
|
-
identity = control = subscription = nil
|
|
177
|
+
identity = control = subscription = latest = nil
|
|
166
178
|
tasks = []
|
|
167
179
|
failure = response = nil
|
|
168
180
|
begin
|
|
@@ -171,8 +183,10 @@ module LibTmux
|
|
|
171
183
|
condition = @arguments.fetch("condition")
|
|
172
184
|
snapshot, pane = resolve(reference)
|
|
173
185
|
require_tracking(snapshot)
|
|
186
|
+
# Daemons below 3.7 take no control client, so a screen wait polls the pane.
|
|
187
|
+
polling = condition.fetch("type") == "screen_contains" && !@server.supports_control?
|
|
174
188
|
identity = acquire_identity(snapshot, pane)
|
|
175
|
-
if condition.fetch("type") == "screen_contains"
|
|
189
|
+
if condition.fetch("type") == "screen_contains" && !polling
|
|
176
190
|
link = snapshot.window_links.find { |item| item.window_id == pane.window_id }
|
|
177
191
|
raise TargetNotFoundError.new("pane has no observable session", phase: :admission) unless link
|
|
178
192
|
|
|
@@ -184,18 +198,38 @@ module LibTmux
|
|
|
184
198
|
control.exchange(line, **@budget.options)
|
|
185
199
|
end
|
|
186
200
|
limits = defaults
|
|
187
|
-
|
|
201
|
+
# A pane or server that ends mid-read ends the wait as PaneDied, with
|
|
202
|
+
# the last rows read, unless the read failed for another reason.
|
|
203
|
+
pane_died = lambda do
|
|
204
|
+
rows, encoding, truncated = latest
|
|
205
|
+
PaneDied.new("observed pane process exited", phase: :observation, delivery: :observed,
|
|
206
|
+
capture: state(reference, rows, encoding, truncated, limits, identity, SecureRandom.hex(16)))
|
|
207
|
+
end
|
|
208
|
+
read = lambda do
|
|
209
|
+
begin
|
|
210
|
+
latest = read_rows(snapshot, pane, limits, identity, true)
|
|
211
|
+
rescue TargetNotFoundError, CommandError, TransportError
|
|
212
|
+
raise if latest.nil?
|
|
213
|
+
|
|
214
|
+
raise unless ended?(identity)
|
|
215
|
+
|
|
216
|
+
raise pane_died.call
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
rows, encoding, truncated = read.call
|
|
188
220
|
if condition.fetch("type") == "screen_contains" && contains?(rows, encoding, condition.fetch("text"))
|
|
189
221
|
next {"target" => reference, "condition" => "screen_contains",
|
|
190
222
|
"capture" => state(reference, rows, encoding, truncated, limits, identity, SecureRandom.hex(16))}
|
|
191
223
|
end
|
|
192
224
|
changed = ::Async::Notification.new
|
|
193
225
|
pending = nil
|
|
226
|
+
weights = {read: 0, stream_ended: 1, exited: 2, server_exited: 2}
|
|
227
|
+
weigh = ->(value) { value.is_a?(Exception) ? 3 : weights.fetch(value) }
|
|
194
228
|
wake = lambda do |value|
|
|
195
|
-
pending = value if pending.nil? ||
|
|
229
|
+
pending = value if pending.nil? || weigh.call(value) > weigh.call(pending)
|
|
196
230
|
changed.signal
|
|
197
231
|
end
|
|
198
|
-
[[identity.io, :exited], [identity.peer,
|
|
232
|
+
[[identity.io, :exited], [identity.peer, :server_exited]].each do |io, event|
|
|
199
233
|
task = ::Async::Task.new(::Async::Task.current) do
|
|
200
234
|
Fiber.scheduler.io_wait(io, IO::READABLE)
|
|
201
235
|
wake.call(event)
|
|
@@ -207,10 +241,30 @@ module LibTmux
|
|
|
207
241
|
tasks << task
|
|
208
242
|
task.run
|
|
209
243
|
end
|
|
210
|
-
if
|
|
244
|
+
if polling
|
|
211
245
|
task = ::Async::Task.new(::Async::Task.current) do
|
|
212
246
|
loop do
|
|
213
|
-
|
|
247
|
+
::Async::Task.current.sleep(POLL_SECONDS)
|
|
248
|
+
wake.call(:read)
|
|
249
|
+
end
|
|
250
|
+
rescue ::Async::Cancel
|
|
251
|
+
nil
|
|
252
|
+
rescue StandardError => error
|
|
253
|
+
wake.call(error)
|
|
254
|
+
end
|
|
255
|
+
tasks << task
|
|
256
|
+
task.run
|
|
257
|
+
end
|
|
258
|
+
if subscription
|
|
259
|
+
task = ::Async::Task.new(::Async::Task.current) do
|
|
260
|
+
# Kernel#loop would swallow the StopIteration that ends the stream.
|
|
261
|
+
while true
|
|
262
|
+
begin
|
|
263
|
+
event = subscription.next(timeout: @budget.options.fetch(:timeout))
|
|
264
|
+
rescue StopIteration
|
|
265
|
+
wake.call(:stream_ended)
|
|
266
|
+
break
|
|
267
|
+
end
|
|
214
268
|
if event.kind == :gap
|
|
215
269
|
wake.call(LostObservation.new("screen observation has a gap", phase: :observation, delivery: :observed))
|
|
216
270
|
break
|
|
@@ -219,8 +273,6 @@ module LibTmux
|
|
|
219
273
|
end
|
|
220
274
|
rescue ::Async::Cancel
|
|
221
275
|
nil
|
|
222
|
-
rescue StopIteration
|
|
223
|
-
wake.call(LostObservation.new("screen observation ended", phase: :observation, delivery: :observed))
|
|
224
276
|
rescue StandardError => error
|
|
225
277
|
wake.call(error)
|
|
226
278
|
end
|
|
@@ -233,15 +285,32 @@ module LibTmux
|
|
|
233
285
|
event, pending = pending, nil
|
|
234
286
|
raise event if event.is_a?(Exception)
|
|
235
287
|
|
|
236
|
-
if event == :
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
288
|
+
if event == :stream_ended
|
|
289
|
+
# A stopped second client keeps a killed server alive after the
|
|
290
|
+
# stream ends, so ask the daemon instead of waiting for its process.
|
|
291
|
+
raise pane_died.call if ended?(identity)
|
|
292
|
+
|
|
293
|
+
raise LostObservation.new("screen observation ended", phase: :observation, delivery: :observed)
|
|
294
|
+
end
|
|
295
|
+
if event == :exited || event == :server_exited
|
|
296
|
+
if condition.fetch("type") == "process_exit"
|
|
297
|
+
begin
|
|
298
|
+
identity.peer_alive!
|
|
299
|
+
rescue TargetNotFoundError
|
|
300
|
+
raise LostObservation.new("tmux process ended", phase: :observation, delivery: :observed), cause: nil
|
|
301
|
+
end
|
|
302
|
+
break {"target" => reference, "condition" => "process_exit", "process_generation" => identity.generation,
|
|
303
|
+
"observed_at" => clock, "exit_status" => "unobserved"}
|
|
304
|
+
end
|
|
305
|
+
# The last output may have landed just before the end.
|
|
306
|
+
rows, encoding, truncated = read.call
|
|
307
|
+
if contains?(rows, encoding, condition.fetch("text"))
|
|
308
|
+
break {"target" => reference, "condition" => "screen_contains",
|
|
309
|
+
"capture" => state(reference, rows, encoding, truncated, limits, identity, SecureRandom.hex(16))}
|
|
240
310
|
end
|
|
241
|
-
|
|
242
|
-
"observed_at" => clock, "exit_status" => "unobserved"}
|
|
311
|
+
raise pane_died.call
|
|
243
312
|
end
|
|
244
|
-
rows, encoding, truncated =
|
|
313
|
+
rows, encoding, truncated = read.call
|
|
245
314
|
if contains?(rows, encoding, condition.fetch("text"))
|
|
246
315
|
break {"target" => reference, "condition" => "screen_contains",
|
|
247
316
|
"capture" => state(reference, rows, encoding, truncated, limits, identity, SecureRandom.hex(16))}
|
|
@@ -271,6 +340,40 @@ module LibTmux
|
|
|
271
340
|
|
|
272
341
|
private
|
|
273
342
|
|
|
343
|
+
# What a daemon that is killed, or shutting down behind a stopped client,
|
|
344
|
+
# tells a client.
|
|
345
|
+
SERVER_GONE = /\A(?:no server running|server exited unexpectedly|error connecting to .*\((?:No such file or directory|Connection refused)\))/
|
|
346
|
+
|
|
347
|
+
# True when the pane process is gone or the daemon says it is gone.
|
|
348
|
+
# An inconclusive probe, such as a failed spawn, is not an end.
|
|
349
|
+
def ended?(identity)
|
|
350
|
+
gone?(identity) || daemon_state == :gone
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# :alive when the daemon answers an ordinary command, :gone when it
|
|
354
|
+
# reports that it is gone, and :unknown when the probe itself fails. The
|
|
355
|
+
# identity read is a run-shell -C probe, which fires no hook.
|
|
356
|
+
def daemon_state
|
|
357
|
+
argv, marker = LibTmux::Internal::TmuxVersion.identity_argv(%w[pid])
|
|
358
|
+
result = @server.run(argv, **@budget.options)
|
|
359
|
+
begin
|
|
360
|
+
LibTmux::Internal::TmuxVersion.parse_identity(result, marker, 1)
|
|
361
|
+
:alive
|
|
362
|
+
rescue ProtocolError
|
|
363
|
+
SERVER_GONE.match?(result.stderr.b.force_encoding(Encoding::UTF_8)) ? :gone : :unknown
|
|
364
|
+
end
|
|
365
|
+
rescue Cancelled, DeadlineExceeded
|
|
366
|
+
raise
|
|
367
|
+
rescue LibTmux::Error, SystemCallError, IOError
|
|
368
|
+
:unknown
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def gone?(identity)
|
|
372
|
+
identity.exited?
|
|
373
|
+
rescue TargetNotFoundError
|
|
374
|
+
true
|
|
375
|
+
end
|
|
376
|
+
|
|
274
377
|
def acquire_identity(snapshot, pane)
|
|
275
378
|
# tmux 3.8 reports no PID for a dead pane; there is no process to bind.
|
|
276
379
|
raise TargetNotFoundError.new("pane process has exited", phase: :admission) if pane.pid.nil?
|
|
@@ -306,7 +409,9 @@ module LibTmux
|
|
|
306
409
|
phase: :admission)
|
|
307
410
|
end
|
|
308
411
|
|
|
309
|
-
|
|
412
|
+
# +joined+ joins wrapped rows into the lines the program wrote, so a wait
|
|
413
|
+
# finds text that the pane width split across rows.
|
|
414
|
+
def read_rows(snapshot, pane, limits, identity, joined = false)
|
|
310
415
|
identity&.ensure_live!
|
|
311
416
|
names = spellings
|
|
312
417
|
link = snapshot.window_links.find { |item| item.window_id == pane.window_id }
|
|
@@ -315,7 +420,7 @@ module LibTmux
|
|
|
315
420
|
target = "#{link.session_id}:.#{pane.id}"
|
|
316
421
|
guard = "\#{&&:\#{==:\#{session_id},#{link.session_id}},\#{==:\#{pane_id},#{pane.id}}}"
|
|
317
422
|
guard = "\#{&&:#{guard},\#{&&:\#{==:\#{pane_pid},#{identity.pid}},\#{&&:\#{==:\#{pane_dead_status},},\#{==:\#{pane_dead_signal},}}}}" if identity
|
|
318
|
-
body = @server.__send__(:tmux_command, [names.fetch("capture-pane"), "-p", "-t", target,
|
|
423
|
+
body = @server.__send__(:tmux_command, [names.fetch("capture-pane"), "-p", *(joined ? ["-J"] : []), "-t", target,
|
|
319
424
|
"-S", (-limits.fetch("history_lines")).to_s, "-E", "-"])
|
|
320
425
|
# The selected false branch fails parsing, independently of screen bytes.
|
|
321
426
|
failure = @server.__send__(:tmux_command, [names.fetch("capture-pane"), "-t"])
|
|
@@ -193,9 +193,9 @@ module LibTmux
|
|
|
193
193
|
end
|
|
194
194
|
# Registration may find a recycled PID. A fresh pinned-route reply must
|
|
195
195
|
# name that same daemon while its retained process observer stays live.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
unless
|
|
196
|
+
argv, marker = LibTmux::Internal::TmuxVersion.identity_argv(%w[pid])
|
|
197
|
+
reported = LibTmux::Internal::TmuxVersion.parse_identity(server.run(argv, **budget.options), marker, 1).first
|
|
198
|
+
unless reported == server_pid.to_s && !readable?(peer)
|
|
199
199
|
raise UnsupportedFeatureError.new("bound response does not establish the tmux process", phase: :admission)
|
|
200
200
|
end
|
|
201
201
|
budget.options
|
data/lib/libtmux/mcp/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: libtmux-mcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.alpha.
|
|
4
|
+
version: 0.1.0.alpha.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- libtmux contributors
|
|
@@ -15,28 +15,28 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.1.0.alpha.
|
|
18
|
+
version: 0.1.0.alpha.3
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.1.0.alpha.
|
|
25
|
+
version: 0.1.0.alpha.3
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: libtmux-async
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
30
|
- - '='
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 0.1.0.alpha.
|
|
32
|
+
version: 0.1.0.alpha.3
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - '='
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.1.0.alpha.
|
|
39
|
+
version: 0.1.0.alpha.3
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: mcp
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -118,7 +118,7 @@ licenses:
|
|
|
118
118
|
- MIT
|
|
119
119
|
metadata:
|
|
120
120
|
source_code_uri: https://github.com/libtmux/libtmux-ruby
|
|
121
|
-
documentation_uri: https://libtmux.org/en/ruby/v0.1.0.alpha.
|
|
121
|
+
documentation_uri: https://libtmux.org/en/ruby/v0.1.0.alpha.3/mcp/
|
|
122
122
|
rdoc_options: []
|
|
123
123
|
require_paths:
|
|
124
124
|
- lib
|