Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
86d2b90
docs(chore[_ext]): add package marker for bundled extensions
tony Apr 20, 2026
857abeb
docs(refactor[widgets]): switch widget imports to docs._ext.widgets
tony Apr 20, 2026
b0c59aa
mypy(chore[config]): drop docs/_ext alias from mypy path
tony Apr 20, 2026
74f2d2d
docs(fix[widgets]): update widget template code reference
tony Apr 20, 2026
bbfffae
mcp(docs[instructions]): add hook, buffer, and is_caller disambiguato…
tony Apr 20, 2026
25272a0
mcp(refactor[display_message]): sharpen description + title for LLM d…
tony Apr 20, 2026
5e52ade
mcp(refactor[pipe_pane]): lead docstring with a concrete logging use …
tony Apr 20, 2026
ae89b9c
mcp(feat[window_tools]): add get_window_info for single-window metadata
tony Apr 20, 2026
8c251b6
mcp(feat[session_tools]): add get_session_info for single-session met…
tony Apr 20, 2026
d5362a9
mcp(feat[pane_tools]): add respawn_pane for in-place shell recovery
tony Apr 20, 2026
8857702
scripts(feat[mcp_swap]): add cross-CLI MCP config detect/swap/revert …
tony Apr 20, 2026
63d344b
mcp(fix[pane_tools]): refuse to respawn the caller's own pane
tony Apr 26, 2026
c24ce53
scripts(fix[mcp_swap]): route save_state through atomic_write
tony Apr 26, 2026
8170c25
mcp(docs[pane_tools]): list respawn in lifecycle module docstring
tony Apr 26, 2026
48ae1fc
mcp(fix[pane_tools]): mark respawn destructive non-idempotent
tony Apr 26, 2026
d9583be
mcp(fix[pane_tools]): require explicit pane for respawn
tony Apr 26, 2026
3ad5ff7
mcp(docs[safety]): document respawn pane risks
tony Apr 26, 2026
bec9ab1
mcp(api[pane_tools]): rename respawn shell parameter
tony Apr 26, 2026
9e1997b
mcp(fix[middleware]): redact respawn shell payloads
tony Apr 26, 2026
3be1786
mcp(docs[pane_tools]): explain respawn command capture
tony Apr 26, 2026
31ff220
mcp(refactor[pane_tools]): drop redundant pane targets
tony Apr 26, 2026
d61bc88
docs(fix[get-window-info]): point users to list_windows
tony Apr 26, 2026
8afc779
scripts(fix[mcp_swap]): align python floor with project
tony Apr 26, 2026
42307e0
mcp(docs[tools]): drop internal audit references
tony Apr 26, 2026
b9db6d5
docs(safety): refresh tmux socket guard notes
tony Apr 26, 2026
909552d
scripts(typing[mcp_swap]): overload claude project lookup
tony Apr 26, 2026
21ad23e
scripts(fix[mcp_swap]): guard claude config shape
tony Apr 26, 2026
cafb64f
mcp(test[server]): verify socket_name instruction contract
tony Apr 26, 2026
efb7bcb
mcp(docs[copy_mode]): note pane.cmd injects -t pane_id
tony Apr 26, 2026
96fed9f
mcp(docs[lifecycle]): mark respawn argv shape as stopgap
tony Apr 26, 2026
c2dd6ef
mcp(docs[changes]): reframe respawn shell rename as pre-release decision
tony Apr 26, 2026
e5926bd
mcp(refactor[server_tools]): centralize socket_name exemption
tony Apr 26, 2026
6432646
mcp(refactor[server]): structure base instructions as composable segm…
tony Apr 26, 2026
58a0ce2
mcp(feat[pane_tools]): expose respawn-pane environment override
tony Apr 26, 2026
6488a9a
scripts(docs[mcp_swap]): clarify global-config + simple-detection scope
tony Apr 26, 2026
199404a
mcp(api[pane_tools]): drop respawn-pane resolver fallbacks
tony Apr 26, 2026
62a200a
scripts(fix[mcp_swap]): preserve existing env on use-local replacement
tony Apr 26, 2026
7216f25
docs(CHANGES) Trim pre-release block to high-level deliveries
tony Apr 26, 2026
0820805
scripts(refactor[mcp_swap]): rename state dir to libtmux-mcp-dev unde…
tony Apr 26, 2026
d8f0098
scripts(typing[mcp_swap]): narrow claude project node via isinstance
tony Apr 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@
_Notes on upcoming releases will be added here_
<!-- END PLACEHOLDER - ADD NEW CHANGELOG ENTRIES BELOW THIS LINE -->

### What's new

#### New tool: `respawn_pane`

Restart a wedged pane in place — preserves `pane_id` and the window layout (the alternative `kill_pane` + `split_window` invalidates pane references and rearranges the window). Required `pane_id`; optional `kill` (default true), `shell`, `start_directory`, `environment`. Refuses to respawn the MCP server's own pane, mirroring the existing `kill_*` self-guards. Annotations honestly mark it `destructiveHint=true` / `idempotentHint=false` while staying in the `mutating` tier so default-profile agents can use it for shell recovery. Audit log redacts the `shell` argument and digests `environment` values (keys stay visible). (#27)

#### New tools: `get_session_info`, `get_window_info`

Targeted single-object metadata reads — no more `list_*` + filter when you have an ID. Completes the four-level `get_*_info` hierarchy (server / session / window / pane); buffers, hooks, and options have existing read paths and are deliberately excluded. (#27)

#### New dev script: `scripts/mcp_swap.py`

Point Claude / Codex / Cursor / Gemini at a local checkout in one command. `just mcp-use-local` rewrites each CLI's global config to run the local repo via `uv`; `just mcp-revert` restores from a timestamped backup. `just mcp-detect` and `just mcp-status` report install state. Atomic writes, `--dry-run` mode, per-CLI state file, env preservation on replacement, layout-shape guard before mutating Claude's per-project config. Scope is global configs only — see `scripts/README.md`. (#27)

### Documentation

- {ref}`safety` adds a {tooliconl}`respawn-pane` "Footgun" subsection alongside {tooliconl}`pipe-pane` and {tooliconl}`set-environment`: `kill=true` default, non-idempotent retries, explicit-`pane_id` requirement, OS-process-table visibility for `shell` / `environment`. The macOS `TMUX_TMPDIR` caveat is rewritten to reflect shipped behaviour — tmux's three-step socket-path resolution is documented, so operators no longer need to set `TMUX_TMPDIR` explicitly to chase a problem that's already solved. (#27)
- LLM-facing discoverability tightened: {tooliconl}`display-message` retitled "Evaluate tmux Format String" with a docstring that leads with read-only format expansion (the tool wraps `display-message -p` but `-p` expands rather than displays), {tooliconl}`pipe-pane` leads with the `/tmp/pane.log` logging use case, and the server instructions explain why hooks are read-only and why there is no `list_buffers` tool. The `socket_name` contract is tightened to acknowledge {tooliconl}`list-servers` as the documented exception. (#27)
- Topic pages added for {tooliconl}`respawn-pane`, {tooliconl}`get-session-info`, and {tooliconl}`get-window-info`. (#27)

### API decisions (pre-release)

- {tooliconl}`respawn-pane` settles on `shell` (renamed from `shell_command`) to align with {tooliconl}`split-window` and upstream `Pane.respawn(shell=)`, and drops the `session_name` / `session_id` / `window_id` resolver fallbacks — the runtime guard rejected any call missing `pane_id` anyway. Validation now lives at the FastMCP schema boundary. (#27)

## libtmux-mcp 0.1.0a3 (2026-04-19)

_Post-0.1.0a2 smoke-test fixes and `libtmux` floor bump_
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ Give your AI agent hands inside the terminal — create sessions, run commands,
| Module | Tools |
|--------|-------|
| **Server** | `list_sessions`, `create_session`, `kill_server`, `get_server_info` |
| **Session** | `list_windows`, `create_window`, `rename_session`, `select_window`, `kill_session` |
| **Window** | `list_panes`, `split_window`, `rename_window`, `select_layout`, `resize_window`, `move_window`, `kill_window` |
| **Pane** | `send_keys`, `paste_text`, `capture_pane`, `snapshot_pane`, `search_panes`, `get_pane_info`, `wait_for_text`, `wait_for_content_change`, `display_message`, `select_pane`, `swap_pane`, `resize_pane`, `set_pane_title`, `clear_pane`, `pipe_pane`, `enter_copy_mode`, `exit_copy_mode`, `kill_pane` |
| **Session** | `list_windows`, `get_session_info`, `create_window`, `rename_session`, `select_window`, `kill_session` |
| **Window** | `list_panes`, `get_window_info`, `split_window`, `rename_window`, `select_layout`, `resize_window`, `move_window`, `kill_window` |
| **Pane** | `send_keys`, `paste_text`, `capture_pane`, `snapshot_pane`, `search_panes`, `get_pane_info`, `wait_for_text`, `wait_for_content_change`, `display_message`, `select_pane`, `swap_pane`, `resize_pane`, `set_pane_title`, `clear_pane`, `pipe_pane`, `enter_copy_mode`, `exit_copy_mode`, `respawn_pane`, `kill_pane` |
| **Options** | `show_option`, `set_option` |
| **Environment** | `show_environment`, `set_environment` |

Expand Down
3 changes: 3 additions & 0 deletions docs/_ext/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Sphinx extensions bundled with the project documentation."""

from __future__ import annotations
3 changes: 2 additions & 1 deletion docs/_widgets/mcp-install/widget.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
MCPInstallWidget.context() / the directive's option merge.

Each code block runs through the `highlight` filter (defined in
widgets._base.make_highlight_filter) which wraps Sphinx's PygmentsBridge —
docs._ext.widgets._base.make_highlight_filter) which wraps Sphinx's
PygmentsBridge —
so the output is byte-identical to a native ``.. code-block::`` block,
meaning sphinx-copybutton + its prompt-strip regex work automatically.
#}
Expand Down
4 changes: 2 additions & 2 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
project_root = cwd.parent
project_src = project_root / "src"

sys.path.insert(0, str(project_root))
sys.path.insert(0, str(project_src))
sys.path.insert(0, str(cwd / "_ext"))

# package data
about: dict[str, str] = {}
Expand All @@ -40,7 +40,7 @@
"sphinx_autodoc_api_style",
"sphinx.ext.todo",
"sphinx_autodoc_fastmcp",
"widgets",
"docs._ext.widgets",
],
intersphinx_mapping={
"python": ("https://docs.python.org/", None),
Expand Down
8 changes: 4 additions & 4 deletions docs/tools/pane/display-message.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# Display message
# Evaluate tmux format string (display_message)

```{fastmcp-tool} pane_tools.display_message
```

**Use when** you need to query arbitrary tmux variables — zoom state, pane
dead flag, client activity, or any `#{format}` string that isn't covered by
other tools.
other tools. Despite the historical name (`display_message` is the tmux verb
it wraps), this tool does **not** display anything to the user; it expands
the format string with `display-message -p` and returns the value.

**Avoid when** a dedicated tool already provides the information — e.g. use
{tooliconl}`snapshot-pane` for cursor position and mode, or
Expand Down Expand Up @@ -33,5 +35,3 @@ zoomed=0 dead=0

```{fastmcp-tool-input} pane_tools.display_message
```

## Act
5 changes: 5 additions & 0 deletions docs/tools/pane/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ Block until a tmux wait-for channel is signalled.
Signal a waiting channel.
:::

:::{grid-item-card} {tooliconl}`respawn-pane`
Restart a pane's process in place, preserving pane_id.
:::

:::{grid-item-card} {tooliconl}`kill-pane`
Terminate a pane. Destructive.
:::
Expand Down Expand Up @@ -110,5 +114,6 @@ wait-for-text
wait-for-content-change
wait-for-channel
signal-channel
respawn-pane
kill-pane
```
83 changes: 83 additions & 0 deletions docs/tools/pane/respawn-pane.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Respawn pane

```{fastmcp-tool} pane_tools.respawn_pane
```

**Use when** a pane's shell or command has wedged (hung REPL, runaway
process, bad terminal mode) and you need a clean restart *without*
destroying the `pane_id` references other tools or callers may still
be holding. With `kill=True` (the default) tmux kills the current
process first; optional `shell` relaunches with a different command;
optional `start_directory` sets its cwd; optional `environment` adds
per-process env vars (one `-e KEY=VALUE` flag per entry).

**Avoid when** the pane genuinely needs to go away — use
{tooliconl}`kill-pane` instead. Also avoid when you want to change
the layout: `respawn-pane` preserves the pane in place.

**Side effects:** Kills the current process (with `kill=True`) and
starts a new one. **The `pane_id` is preserved** — that's the whole
point of the tool. `pane_pid` updates to the new process.

**Tip:** Call {tooliconl}`get-pane-info` first if you need to capture
`pane_current_command` before respawn — the new process loses its argv.
Omitting `shell` makes tmux replay the original argv (good default for
shells; may differ for processes spawned via custom shell at split
time).

**Example — recover a wedged pane, relaunching the default shell:**

```json
{
"tool": "respawn_pane",
"arguments": {
"pane_id": "%5"
}
}
```

**Example — relaunch with a different command and working directory:**

```json
{
"tool": "respawn_pane",
"arguments": {
"pane_id": "%5",
"shell": "pytest -x",
"start_directory": "/home/user/project"
}
}
```

**Example — relaunch with extra environment variables:**

```json
{
"tool": "respawn_pane",
"arguments": {
"pane_id": "%5",
"shell": "pytest -x",
"environment": {
"PYTHONPATH": "/home/user/project/src",
"DATABASE_URL": "postgres://localhost/test"
}
}
}
```

The audit log redacts each `environment` *value* via `{len, sha256_prefix}` digests but keeps the keys visible (env var names like `DATABASE_URL` are operator-debug-useful, while their values are the secret). Note that values may still appear briefly in the OS process table while tmux spawns the new process — see {ref}`safety` for details.

Response (PaneInfo):

```json
{
"pane_id": "%5",
"pane_pid": "98765",
"pane_current_command": "pytest",
"pane_current_path": "/home/user/project",
...
}
```

```{fastmcp-tool-input} pane_tools.respawn_pane
```
51 changes: 51 additions & 0 deletions docs/tools/session/get-session-info.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Get session info

```{fastmcp-tool} session_tools.get_session_info
```

**Use when** you need metadata for a single session (ID, name, window
count, attachment status, activity timestamp) and you already know its
`session_id` or `session_name`. Avoids the `list_sessions` + filter dance.

**Avoid when** you need every session — call `list_sessions` or iterate
via the `tmux://sessions` resource.

**Side effects:** None. Readonly.

**Example:**

```json
{
"tool": "get_session_info",
"arguments": {
"session_id": "$0"
}
}
```

Response:

```json
{
"session_id": "$0",
"session_name": "dev",
"window_count": 3,
"session_attached": "1",
"session_created": "1713600000",
"active_pane_id": "%0"
}
```

Resolve by name when only the session_name is known:

```json
{
"tool": "get_session_info",
"arguments": {
"session_name": "dev"
}
}
```

```{fastmcp-tool-input} session_tools.get_session_info
```
5 changes: 5 additions & 0 deletions docs/tools/session/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Session-scoped tools — enumerate windows, rename or kill a session, switch win
Enumerate windows inside a session.
:::

:::{grid-item-card} {tooliconl}`get-session-info`
Read metadata for one session.
:::

:::{grid-item-card} {tooliconl}`select-window`
Switch to a window by id, index, or direction.
:::
Expand All @@ -32,6 +36,7 @@ Terminate a session. Destructive.
:maxdepth: 1

list-windows
get-session-info
select-window
create-window
rename-session
Expand Down
57 changes: 57 additions & 0 deletions docs/tools/window/get-window-info.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Get window info

```{fastmcp-tool} window_tools.get_window_info
```

**Use when** you need metadata for a single window (name, index, layout,
dimensions, pane count) and you already know the `window_id` or
`window_index`. Avoids the `list_windows` + filter dance.

**Avoid when** you need every window in a session — call `list_windows` with
`session_id` or iterate via the `tmux://sessions/{name}/windows` resource.

**Side effects:** None. Readonly.

**Example:**

```json
{
"tool": "get_window_info",
"arguments": {
"window_id": "@1"
}
}
```

Response:

```json
{
"window_id": "@1",
"window_name": "editor",
"window_index": "1",
"session_id": "$0",
"session_name": "dev",
"pane_count": 2,
"window_layout": "7f9f,80x24,0,0[80x15,0,0,0,80x8,0,16,1]",
"window_active": "1",
"window_width": "80",
"window_height": "24"
}
```

Resolve by `window_index` when only the index is known — requires
`session_name` or `session_id` to disambiguate:

```json
{
"tool": "get_window_info",
"arguments": {
"window_index": "1",
"session_name": "dev"
}
}
```

```{fastmcp-tool-input} window_tools.get_window_info
```
5 changes: 5 additions & 0 deletions docs/tools/window/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ Window-scoped tools — enumerate panes, split / rename / relayout / resize / mo
Enumerate panes inside a window.
:::

:::{grid-item-card} {tooliconl}`get-window-info`
Read metadata for one window.
:::

:::{grid-item-card} {tooliconl}`split-window`
Split a window into a new pane.
:::
Expand Down Expand Up @@ -40,6 +44,7 @@ Terminate a window. Destructive.
:maxdepth: 1

list-panes
get-window-info
split-window
rename-window
select-layout
Expand Down
Loading
Loading