Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:17:58 +08:00
commit 72ca635273
15 changed files with 5327 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
{
"name": "tmux",
"description": "Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output. Use when debugging applications, running interactive REPLs (Python, gdb, ipdb, psql, mysql, node), automating terminal workflows, or when user mentions tmux, debugging, or interactive shells.",
"version": "1.4.0",
"author": {
"name": "Alberto Leal"
},
"skills": [
"./"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# tmux
Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output. Use when debugging applications, running interactive REPLs (Python, gdb, ipdb, psql, mysql, node), automating terminal workflows, or when user mentions tmux, debugging, or interactive shells.

622
SKILL.md Normal file
View File

@@ -0,0 +1,622 @@
---
name: tmux
description: "Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output. Use when debugging applications, running interactive REPLs (Python, gdb, ipdb, psql, mysql, node), automating terminal workflows, or when user mentions tmux, debugging, or interactive shells."
license: Vibecoded
---
# tmux Skill
Use tmux as a programmable terminal multiplexer for interactive work. Works on Linux and macOS with stock tmux; avoid custom config by using a private socket.
## Quickstart
The session registry eliminates repetitive socket/target specification through automatic session tracking (~80% reduction in boilerplate):
**IMPORTANT**: Before creating a new session, ALWAYS check existing sessions first to avoid name conflicts:
```bash
# Check existing sessions to ensure name is available
./tools/list-sessions.sh
# Create and register a Python REPL session (choose a unique name)
./tools/create-session.sh -n claude-python --python
# Send commands using session name (auto-lookup socket/target)
./tools/safe-send.sh -s claude-python -c "print(2+2)" -w ">>>"
# Or with a single session, omit -s entirely (auto-detect)
./tools/safe-send.sh -c "print('hello world')" -w ">>>"
# List all registered sessions with health status
./tools/list-sessions.sh
# Clean up dead sessions
./tools/cleanup-sessions.sh
```
After starting a session, ALWAYS tell the user how to monitor it by giving them a command to copy/paste (substitute actual values from the session you created):
```
To monitor this session yourself:
./tools/list-sessions.sh
Or attach directly:
tmux -S <socket> attach -t <session-name>
Or to capture the output once:
tmux -S <socket> capture-pane -p -J -t <session-name>:0.0 -S -200
```
This must ALWAYS be printed right after a session was started (i.e. right before you start using the session) and once again at the end of the tool loop. But the earlier you send it, the happier the user will be.
## How It Works
The session registry provides three ways to reference sessions:
1. **By name** using `-s session-name` (looks up socket/target in registry)
2. **Auto-detect** when only one session exists (omit `-s`)
3. **Explicit** using `-S socket -t target` (backward compatible)
Tools automatically choose the right session using this priority order:
1. Explicit `-S` and `-t` flags (highest priority)
2. Session name `-s` flag (registry lookup)
3. Auto-detect single session (if only one exists)
**Benefits:**
- No more repeating `-S socket -t target` on every command
- Automatic session discovery
- Built-in health tracking
- Activity timestamps for cleanup decisions
- Fully backward compatible
## Common Workflows
For practical examples of managing tmux sessions through their lifecycle, see the [Session Lifecycle Guide](./references/session-lifecycle.md).
This guide covers:
- **Daily workflows**: Ephemeral sessions, long-running analysis, crash recovery, multi-session workspaces
- **Decision trees**: Create vs reuse, cleanup timing, error handling
- **Tool reference matrix**: Which tools to use at each lifecycle stage
- **Troubleshooting**: Quick fixes for common problems (session not found, commands not executing, cleanup issues)
- **Best practices**: 10 DO's and 10 DON'Ts with examples
## Finding sessions
List all registered sessions with health status:
```bash
./tools/list-sessions.sh # Table format
./tools/list-sessions.sh --json # JSON format
```
Output shows session name, socket, target, health status, PID, and creation time.
## Sending input safely
The `./tools/safe-send.sh` helper provides automatic retries, readiness checks, and optional prompt waiting:
```bash
# Using session name (looks up socket/target from registry)
./tools/safe-send.sh -s claude-python -c "print('hello')" -w ">>>"
# Auto-detect single session (omit -s)
./tools/safe-send.sh -c "print('world')" -w ">>>"
# Explicit socket/target (backward compatible)
./tools/safe-send.sh -S "$SOCKET" -t "$SESSION":0.0 -c "print('hello')" -w ">>>"
```
See the [Helper: safe-send.sh](#helper-safe-sendsh) section below for full documentation.
## Watching output
- Capture recent history (joined lines to avoid wrapping artifacts): `tmux -S "$SOCKET" capture-pane -p -J -t target -S -200`.
- For continuous monitoring, poll with the helper script (below) instead of `tmux wait-for` (which does not watch pane output).
- You can also temporarily attach to observe: `tmux -S "$SOCKET" attach -t "$SESSION"`; detach with `Ctrl+b d`.
- When giving instructions to a user, **explicitly print a copy/paste monitor command** alongside the action—don't assume they remembered the command.
## Spawning Processes
Some special rules for processes:
- when asked to debug, use lldb by default
- **CRITICAL**: When starting a Python interactive shell, **always** set the `PYTHON_BASIC_REPL=1` environment variable before launching Python. This is **essential** - the non-basic console (fancy REPL with syntax highlighting) interferes with send-keys and will cause commands to fail silently.
```bash
# When using create-session.sh, this is automatic with --python flag
./tools/create-session.sh -n my-python --python
# When creating manually:
tmux -S "$SOCKET" send-keys -t "$SESSION":0.0 -- 'PYTHON_BASIC_REPL=1 python3 -q' Enter
```
## Synchronizing / waiting for prompts
Use timed polling to avoid races with interactive tools:
```bash
# Wait for Python prompt
./tools/wait-for-text.sh -s claude-python -p '^>>>' -T 15 -l 4000
# Auto-detect single session
./tools/wait-for-text.sh -p '^>>>' -T 15
# Explicit socket/target
./tools/wait-for-text.sh -S "$SOCKET" -t "$SESSION":0.0 -p '^>>>' -T 15 -l 4000
```
For long-running commands, poll for completion text (`"Type quit to exit"`, `"Program exited"`, etc.) before proceeding.
## Interactive tool recipes
- **Python REPL**: Use `./tools/create-session.sh -n my-python --python`; wait for `^>>>`; send code; interrupt with `C-c`. The `--python` flag automatically sets `PYTHON_BASIC_REPL=1`.
- **gdb**: Use `./tools/create-session.sh -n my-gdb --gdb`; disable paging with safe-send; break with `C-c`; issue `bt`, `info locals`, etc.; exit via `quit` then confirm `y`.
- **Other TTY apps** (ipdb, psql, mysql, node, bash): Use `./tools/create-session.sh -n my-session --shell`; poll for prompt; send literal text and Enter.
## Cleanup
Killing sessions (recommended - removes both tmux session and registry entry):
```bash
# Kill a specific session by name
./tools/kill-session.sh -s session-name
# Auto-detect and kill single session
./tools/kill-session.sh
# Dry-run to see what would be killed
./tools/kill-session.sh -s session-name --dry-run
```
Registry cleanup (removes registry entries only, doesn't kill tmux sessions):
```bash
# Remove dead sessions from registry
./tools/cleanup-sessions.sh
# Remove sessions older than 1 hour
./tools/cleanup-sessions.sh --older-than 1h
# See what would be removed (dry-run)
./tools/cleanup-sessions.sh --dry-run
```
Manual cleanup (when not using registry):
- Kill a session when done: `tmux -S "$SOCKET" kill-session -t "$SESSION"`.
- Kill all sessions on a socket: `tmux -S "$SOCKET" list-sessions -F '#{session_name}' | xargs -r -n1 tmux -S "$SOCKET" kill-session -t`.
- Remove everything on the private socket: `tmux -S "$SOCKET" kill-server`.
## Helper: create-session.sh
`./tools/create-session.sh` creates and registers new tmux sessions with automatic registry integration.
**IMPORTANT**: Before creating a session, ALWAYS run `./tools/list-sessions.sh` to check for existing sessions and ensure your chosen name is unique.
```bash
./tools/create-session.sh -n <name> [--python|--gdb|--shell] [options]
```
**Key options:**
- `-n`/`--name` session name (required)
- `--python` launch Python REPL with PYTHON_BASIC_REPL=1
- `--gdb` launch gdb debugger
- `--shell` launch bash shell (default)
- `-S`/`--socket` custom socket path (optional, uses default)
- `-w`/`--window` window name (default: "shell")
- `--no-register` don't add to registry
**Examples:**
```bash
# Create Python REPL session
./tools/create-session.sh -n claude-python --python
# Create gdb session
./tools/create-session.sh -n claude-gdb --gdb
# Create session without registering
./tools/create-session.sh -n temp-session --shell --no-register
# Create session with custom socket
./tools/create-session.sh -n my-session -S /tmp/custom.sock --python
```
**Returns JSON with session info:**
```json
{
"name": "claude-python",
"socket": "/tmp/claude-tmux-sockets/claude.sock",
"target": "claude-python:0.0",
"type": "python-repl",
"pid": 12345,
"registered": true
}
```
## Helper: list-sessions.sh
`./tools/list-sessions.sh` lists all registered sessions with health status.
```bash
./tools/list-sessions.sh [--json]
```
**Options:**
- `--json` output as JSON instead of table format
**Table output (default):**
```
NAME SOCKET TARGET STATUS PID CREATED
claude-python claude.sock :0.0 alive 1234 2h ago
claude-gdb claude.sock :0.0 dead - 1h ago
Total: 2 | Alive: 1 | Dead: 1
```
**JSON output:**
```json
{
"sessions": [
{"name": "claude-python", "status": "alive", ...}
],
"total": 2,
"alive": 1,
"dead": 1
}
```
**Health statuses:**
- `alive` - Session running and healthy
- `dead` - Pane marked as dead
- `missing` - Session/pane not found
- `zombie` - Process exited but pane exists
- `server` - Tmux server not running
## Helper: cleanup-sessions.sh
`./tools/cleanup-sessions.sh` removes dead or old sessions from the registry.
```bash
./tools/cleanup-sessions.sh [--dry-run] [--all] [--older-than <duration>]
```
**Options:**
- `--dry-run` show what would be cleaned without removing
- `--all` remove all sessions (even alive ones)
- `--older-than <duration>` remove sessions older than threshold (e.g., "1h", "2d")
**Examples:**
```bash
# Remove dead sessions
./tools/cleanup-sessions.sh
# Dry-run to see what would be removed
./tools/cleanup-sessions.sh --dry-run
# Remove sessions inactive for more than 1 hour
./tools/cleanup-sessions.sh --older-than 1h
# Remove all sessions
./tools/cleanup-sessions.sh --all
```
**Duration format:** `30m`, `2h`, `1d`, `3600s`
## Helper: kill-session.sh
Kill tmux session and remove from registry (atomic operation).
**Purpose**: Provides a single operation to fully clean up a session by both killing the tmux session and removing it from the registry.
**Key features**:
- Atomic operation (kills session AND deregisters)
- Three operation modes: registry lookup, explicit socket/target, auto-detect
- Dry-run support for safety
- Proper exit codes for all scenarios
**Usage**:
```bash
# Kill session by name (registry lookup)
tools/kill-session.sh -s claude-python
# Kill with explicit socket and target
tools/kill-session.sh -S /tmp/claude.sock -t my-session:0.0
# Auto-detect single session
tools/kill-session.sh
# Dry-run to see what would happen
tools/kill-session.sh -s claude-python --dry-run
```
**Options**:
- `-s NAME` - Session name (uses registry lookup)
- `-S PATH` - Socket path (explicit mode, requires -t)
- `-t TARGET` - Target pane (explicit mode, requires -S)
- `--dry-run` - Show operations without executing
- `-v` - Verbose output
- `-h` - Show help
**Exit codes**:
- 0 - Complete success (killed AND deregistered)
- 1 - Partial success (one operation succeeded)
- 2 - Complete failure (both failed or not found)
- 3 - Invalid arguments
**Priority order** (when multiple methods specified):
1. Explicit -S and -t (highest priority)
2. Session name -s (registry lookup)
3. Auto-detect (if no flags and only one session exists)
**When to use**:
- Cleaning up after interactive debugging sessions
- Removing sessions that are no longer needed
- Ensuring complete cleanup (both tmux and registry)
- Batch operations with proper error handling
**Notes**:
- Unlike `cleanup-sessions.sh` (which only removes registry entries), this tool also kills the actual tmux session
- Use auto-detect mode when you have only one session and want quick cleanup
- Dry-run mode is helpful to verify what will be cleaned up before executing
## Helper: safe-send.sh
`./tools/safe-send.sh` sends keystrokes to tmux panes with automatic retries, readiness checks, and optional prompt waiting. Prevents dropped commands that can occur when sending to busy or not-yet-ready panes.
```bash
# Session registry mode
./tools/safe-send.sh -s session-name -c "command" [-w pattern]
# Auto-detect mode (single session)
./tools/safe-send.sh -c "command" [-w pattern]
# Explicit mode (backward compatible)
./tools/safe-send.sh -t session:0.0 -c "command" [-S socket] [-w pattern]
```
**Target selection (priority order):**
- `-s`/`--session` session name (looks up socket/target in registry)
- `-t`/`--target` explicit pane target (session:window.pane)
- (no flags) auto-detect if only one session exists
**Key options:**
- `-c`/`--command` command to send (required; empty string sends just Enter)
- `-S`/`--socket` tmux socket path (for custom sockets via -S)
- `-L`/`--socket-name` tmux socket name (for named sockets via -L)
- `-l`/`--literal` use literal mode (send text without executing)
- `-m`/`--multiline` use multiline mode (paste-buffer for code blocks)
- `-w`/`--wait` wait for this pattern after sending
- `-T`/`--timeout` timeout in seconds (default: 30)
- `-r`/`--retries` max retry attempts (default: 3)
- `-i`/`--interval` base retry interval in seconds (default: 0.5)
- `-v`/`--verbose` verbose output for debugging
**Exit codes:**
- `0` - Command sent successfully
- `1` - Failed to send after retries
- `2` - Timeout waiting for prompt
- `3` - Pane not ready
- `4` - Invalid arguments
**Modes:**
- **Normal mode (default):** Sends command and presses Enter (executes in shell/REPL)
- **Multiline mode (-m):** Sends multiline code blocks via paste-buffer (~10x faster than line-by-line). Auto-appends blank line for Python REPL execution. Incompatible with `-l`.
- **Literal mode (-l):** Sends exact characters without Enter (typing text). Incompatible with `-m`.
**Use cases:**
- Send commands to Python REPL with automatic retry and prompt waiting
- Send gdb commands and wait for the gdb prompt
- Critical commands that must not be dropped
- Send commands immediately after session creation
- Automate interactions with any interactive CLI tool
**Examples:**
```bash
# Send Python command using session registry
./tools/safe-send.sh -s claude-python -c "print('hello')" -w ">>>" -T 10
# Auto-detect single session
./tools/safe-send.sh -c "print('world')" -w ">>>"
# Send text in literal mode (no Enter)
./tools/safe-send.sh -s claude-python -c "some text" -l
# Send with custom retry settings
./tools/safe-send.sh -s claude-python -c "ls" -r 5 -i 1.0
# Send control sequence
./tools/safe-send.sh -s claude-python -c "C-c"
# Send multiline Python function (fast, preserves indentation)
./tools/safe-send.sh -s claude-python -m -c "def fibonacci(n):
if n <= 1:
return n
return fibonacci(n-1) + fibonacci(n-2)" -w ">>>" -T 10
# Send multiline class definition
./tools/safe-send.sh -s claude-python -m -c "class Calculator:
def __init__(self):
self.result = 0
def add(self, x):
self.result += x
return self" -w ">>>"
# Explicit socket/target (backward compatible)
SOCKET_DIR=${TMPDIR:-/tmp}/claude-tmux-sockets
SOCKET="$SOCKET_DIR/claude.sock"
./tools/safe-send.sh -S "$SOCKET" -t "$SESSION":0.0 -c "print('hello')" -w ">>>"
```
**Multiline mode benefits:**
- **~10x faster** than sending line-by-line (single operation vs N separate calls)
- **Preserves indentation** perfectly (important for Python)
- **Auto-executes** in Python REPL (blank line appended automatically)
- **Cleaner logs** (one operation instead of many)
- **Best for:** Function definitions, class definitions, complex code blocks
## Helper: wait-for-text.sh
`./tools/wait-for-text.sh` polls a pane for a regex (or fixed string) with a timeout. Works on Linux/macOS with bash + tmux + grep.
```bash
# Using session name (looks up socket/target from registry)
./tools/wait-for-text.sh -s claude-python -p '^>>>' -T 15
# Auto-detect single session (omit -s)
./tools/wait-for-text.sh -p '^>>>' -T 15
# Explicit socket/target (backward compatible)
./tools/wait-for-text.sh -S "$SOCKET" -t "$SESSION":0.0 -p '^>>>' -T 15
```
**Target selection (priority order):**
- `-s`/`--session` session name (looks up socket/target in registry)
- `-t`/`--target` explicit pane target (session:window.pane)
- (no flags) auto-detect if only one session exists
**Options:**
- `-p`/`--pattern` regex to match (required); add `-F` for fixed string
- `-S`/`--socket` tmux socket path (for custom sockets via -S)
- `-T` timeout seconds (integer, default 15)
- `-i` poll interval seconds (default 0.5)
- `-l` history lines to search from the pane (integer, default 1000)
- Exits 0 on first match, 1 on timeout. On failure prints the last captured text to stderr to aid debugging.
**Examples:**
```bash
# Wait for Python prompt using session name
./tools/wait-for-text.sh -s claude-python -p '^>>>' -T 10
# Wait for gdb prompt with auto-detect
./tools/wait-for-text.sh -p '(gdb)' -T 10
# Explicit socket/target (backward compatible)
SOCKET_DIR=${TMPDIR:-/tmp}/claude-tmux-sockets
SOCKET="$SOCKET_DIR/claude.sock"
./tools/wait-for-text.sh -S "$SOCKET" -t "$SESSION":0.0 -p '^>>>' -T 15
```
## Helper: pane-health.sh
`./tools/pane-health.sh` checks the health status of a tmux pane before operations to prevent "pane not found" errors and detect failures early. Essential for reliable automation.
```bash
# Using session name (looks up socket/target from registry)
./tools/pane-health.sh -s claude-python [--format json|text]
# Auto-detect single session (omit -s)
./tools/pane-health.sh --format text
# Explicit socket/target (backward compatible)
./tools/pane-health.sh -S "$SOCKET" -t "$SESSION":0.0 [--format json|text]
```
**Target selection (priority order):**
- `-s`/`--session` session name (looks up socket/target in registry)
- `-t`/`--target` explicit pane target (session:window.pane)
- (no flags) auto-detect if only one session exists
**Options:**
- `-S`/`--socket` tmux socket path (for custom sockets via -S)
- `--format` output format: `json` (default) or `text`
- Exits with status codes indicating health state
**Exit codes:**
- `0` - Healthy (pane alive, process running)
- `1` - Dead (pane marked as dead)
- `2` - Missing (pane/session doesn't exist)
- `3` - Zombie (process exited but pane still exists)
- `4` - Server not running
**JSON output includes:**
- `status`: overall health (`healthy`, `dead`, `missing`, `zombie`, `server_not_running`)
- `server_running`: boolean
- `session_exists`: boolean
- `pane_exists`: boolean
- `pane_dead`: boolean
- `pid`: process ID (or null)
- `process_running`: boolean
**Use cases:**
- Before sending commands: verify pane is ready
- After errors: determine if pane crashed
- Periodic health checks during long operations
- Cleanup decision: which panes to kill vs keep
**Examples:**
```bash
# Check health using session name (JSON output)
./tools/pane-health.sh -s claude-python
# Output: {"status": "healthy", "server_running": true, ...}
# Check health with auto-detect (text output)
./tools/pane-health.sh --format text
# Output: Pane claude-python:0.0 is healthy (PID: 12345, process running)
# Conditional logic with session registry
if ./tools/pane-health.sh -s my-session --format text; then
echo "Pane is ready for commands"
./tools/safe-send.sh -s my-session -c "print('hello')"
else
echo "Pane is not healthy (exit code: $?)"
fi
# Explicit socket/target (backward compatible)
SOCKET_DIR=${TMPDIR:-/tmp}/claude-tmux-sockets
SOCKET="$SOCKET_DIR/claude.sock"
./tools/pane-health.sh -S "$SOCKET" -t "$SESSION":0.0
```
## Advanced: Direct Socket Control
For advanced users who need explicit control over socket paths without using the session registry, see the [Direct Socket Control](references/direct-socket-control.md) reference.
This is useful for:
- Custom socket isolation requirements
- Integration with existing tmux workflows
- Testing or debugging tmux configuration
Most workflows should use the session registry tools described above.
## Best Practices
For comprehensive guidance on using the session registry effectively, see:
- **[Session Registry Reference](references/session-registry.md)** - Complete documentation including:
- Registry architecture and file format
- Advanced usage patterns
- Troubleshooting guide
- Migration from manual socket management
- Best practices for session naming, cleanup strategies, and error handling
- When to use registry vs. manual approach
Key recommendations:
- Use descriptive session names (e.g., `claude-python-analysis`, not `session1`)
- Run `./tools/cleanup-sessions.sh` periodically to remove dead sessions
- Use `./tools/list-sessions.sh` to verify session health before long operations
- For single-session workflows, omit `-s` flag to leverage auto-detection
- For multiple sessions, always use `-s session-name` for clarity
## Troubleshooting
**Session not found in registry:**
- Use `./tools/list-sessions.sh` to see all registered sessions
- Session may have been created with `--no-register` flag
- Registry file may be corrupted (check `$CLAUDE_TMUX_SOCKET_DIR/.sessions.json`)
**Auto-detection fails with "Multiple sessions found":**
- Specify session name explicitly with `-s my-session`
- Or clean up unused sessions with `./tools/cleanup-sessions.sh`
**Pane health check fails:**
- Session may have crashed - check with `./tools/list-sessions.sh`
- Tmux server may not be running - verify socket exists
- Use `./tools/pane-health.sh -s session-name --format text` for detailed diagnostics
**Registry lock timeout:**
- Another process may be writing to registry
- Wait a moment and retry
- Check for stale lock file: `$CLAUDE_TMUX_SOCKET_DIR/.sessions.lock`
For more detailed troubleshooting, see the [Session Registry Reference](references/session-registry.md#troubleshooting).

97
plugin.lock.json Normal file
View File

@@ -0,0 +1,97 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:dashed/claude-marketplace:plugins/tmux",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "b791a79cde3dc378f5f48d0f4768846c62bc5109",
"treeHash": "c812bb0ddafed26011d6f61daa44be29967ae60526e682cb764e5e47dcc9fe6d",
"generatedAt": "2025-11-28T10:16:03.271236Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "tmux",
"description": "Remote control tmux sessions for interactive CLIs (python, gdb, etc.) by sending keystrokes and scraping pane output. Use when debugging applications, running interactive REPLs (Python, gdb, ipdb, psql, mysql, node), automating terminal workflows, or when user mentions tmux, debugging, or interactive shells.",
"version": "1.4.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "f2fdf0bf02e8605e10c4599cb22e8ad567c432cb2edab0ed69bd41d03ef7b8a8"
},
{
"path": "SKILL.md",
"sha256": "8d54919da5b2f19c4c5f56f70f78d1a911f87b648b91cab68f9a2e1b9871c70b"
},
{
"path": "tools/list-sessions.sh",
"sha256": "5cf212b88d78ba58f3d43b9c36d2479ba37819d4ce7fdd4f2ac3af75f8565d40"
},
{
"path": "tools/kill-session.sh",
"sha256": "42a2b18ca43402e0e9880edb97c56cb49a0ee90501a904b357a6267dc8a15e02"
},
{
"path": "tools/wait-for-text.sh",
"sha256": "7fa8851b3be0d49abb49c4fbdaf31e687036ebf1d40030e66e1d5a7d3d72298a"
},
{
"path": "tools/pane-health.sh",
"sha256": "c43f562af38359e26558c2a5548753c0b245549f828082ad4473db734f795a95"
},
{
"path": "tools/safe-send.sh",
"sha256": "f505adb563854400da13f8b59f5de782292301e7736849d9d2a2766b1eb9ab2d"
},
{
"path": "tools/create-session.sh",
"sha256": "066417e810ff7b84200bba96edba07257c920f9d2bfa4eaa34e95bddb2c23cb5"
},
{
"path": "tools/find-sessions.sh",
"sha256": "ecfad81dbb7c9aaecb03fbd01f5175902916be18d68184589145c2ebd5585b35"
},
{
"path": "tools/cleanup-sessions.sh",
"sha256": "2b9dd4b78738b341579c34b032c3898284571cc418bac04d25493c69fb64264c"
},
{
"path": "tools/lib/registry.sh",
"sha256": "3ce5fcf343910795e492a6e7b952fd12ba59624be0aaa36271da69ad77201739"
},
{
"path": "tools/lib/time_utils.sh",
"sha256": "526ffe7bcc4506b0b810d1c6a023bcce8fda22d3e9086eddb926e354729ba869"
},
{
"path": "references/session-lifecycle.md",
"sha256": "489dd66ffbada2c49bb4b3b0ddc20daf2edddba1bf6af01957a4ea5719aa658c"
},
{
"path": "references/session-registry.md",
"sha256": "b2834deabbaabc2ed2b2e8a6e1ee3abb2371776461b9ca01a120a36522636785"
},
{
"path": "references/direct-socket-control.md",
"sha256": "936d7dabdc75bd8e8b10ff4513740d3a9e54757892c283c828557c86fc3b2b4b"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "59f1b332b5913768c9aa80aa3f12f622fef6daf082e2b8ef9053191ec3ea3b50"
}
],
"dirSha256": "c812bb0ddafed26011d6f61daa44be29967ae60526e682cb764e5e47dcc9fe6d"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,108 @@
# Direct Socket Control
This guide covers using direct tmux commands for session management without the session registry. This is an advanced approach for users who need explicit control over socket paths and session management.
**Note:** The session registry is recommended for most workflows. It eliminates ~80% of boilerplate and provides automatic session tracking, health checking, and cleanup. See [Session Registry Reference](session-registry.md) for the standard approach.
## When to Use This Approach
Use direct socket control when you need:
- Custom socket isolation requirements
- Integration with existing tmux workflows
- Multiple sessions on different sockets with complex routing
- Testing or debugging tmux configuration
For most interactive development, debugging, and REPL workflows, use the session registry tools instead.
## Manual Setup
```bash
SOCKET_DIR=${TMPDIR:-/tmp}/claude-tmux-sockets # well-known dir for all agent sockets
mkdir -p "$SOCKET_DIR"
SOCKET="$SOCKET_DIR/claude.sock" # keep agent sessions separate from your personal tmux
SESSION=claude-python # slug-like names; avoid spaces
tmux -S "$SOCKET" new -d -s "$SESSION" -n shell
tmux -S "$SOCKET" send-keys -t "$SESSION":0.0 -- 'PYTHON_BASIC_REPL=1 python3 -q' Enter
tmux -S "$SOCKET" capture-pane -p -J -t "$SESSION":0.0 -S -200 # watch output
tmux -S "$SOCKET" kill-session -t "$SESSION" # clean up
```
After starting a session, ALWAYS tell the user how to monitor it:
```
To monitor this session yourself:
tmux -S "$SOCKET" attach -t claude-python
Or to capture the output once:
tmux -S "$SOCKET" capture-pane -p -J -t claude-python:0.0 -S -200
```
## Socket Convention
- Agents MUST place tmux sockets under `CLAUDE_TMUX_SOCKET_DIR` (defaults to `${TMPDIR:-/tmp}/claude-tmux-sockets`) and use `tmux -S "$SOCKET"` so we can enumerate/clean them. Create the dir first: `mkdir -p "$CLAUDE_TMUX_SOCKET_DIR"`.
- Default socket path to use unless you must isolate further: `SOCKET="$CLAUDE_TMUX_SOCKET_DIR/claude.sock"`.
## Targeting Panes and Naming
- Target format: `{session}:{window}.{pane}`, defaults to `:0.0` if omitted. Keep names short (e.g., `claude-py`, `claude-gdb`).
- Use `-S "$SOCKET"` consistently to stay on the private socket path. If you need user config, drop `-f /dev/null`; otherwise `-f /dev/null` gives a clean config.
- Inspect: `tmux -S "$SOCKET" list-sessions`, `tmux -S "$SOCKET" list-panes -a`.
## Finding Sessions Manually
- List sessions on a specific socket: `./tools/find-sessions.sh -S "$SOCKET"`; add `-q partial-name` to filter.
- Scan all sockets: `./tools/find-sessions.sh --all` (uses `CLAUDE_TMUX_SOCKET_DIR`)
## Direct tmux send-keys
- Prefer literal sends to avoid shell splitting: `tmux -S "$SOCKET" send-keys -t target -l -- "$cmd"`
- When composing inline commands, use single quotes or ANSI C quoting to avoid expansion: `tmux ... send-keys -t target -- $'python3 -m http.server 8000'`.
- To send control keys: `tmux ... send-keys -t target C-c`, `C-d`, `C-z`, `Escape`, etc.
## Comparison with Session Registry
| Feature | Direct Socket Control | Session Registry |
|---------|----------------------|------------------|
| Setup complexity | Manual, verbose | Automated with tools |
| Socket/target specification | Required every time | Once at creation |
| Session discovery | Manual enumeration | Automatic tracking |
| Health checking | Manual verification | Built-in health status |
| Cleanup | Manual kill commands | Automated cleanup tools |
| Auto-detection | Not available | Single session auto-detect |
| Best for | Custom isolation, CI/CD | Interactive workflows |
## Migrating to Session Registry
If you're using direct socket control and want to migrate to the registry approach:
1. **Create sessions with registry tools:**
```bash
# Instead of:
# tmux -S "$SOCKET" new -d -s my-session -n shell
# Use:
./tools/create-session.sh -n my-session --shell
```
2. **Replace socket/target with session name:**
```bash
# Instead of:
# ./tools/safe-send.sh -S "$SOCKET" -t "$SESSION":0.0 -c "command"
# Use:
./tools/safe-send.sh -s my-session -c "command"
```
3. **Use registry for cleanup:**
```bash
# Instead of:
# tmux -S "$SOCKET" kill-session -t "$SESSION"
# Use:
./tools/cleanup-sessions.sh
```
See the [Migration Guide](session-registry.md#migration-guide) in the Session Registry Reference for complete migration instructions.
## See Also
- [Session Registry Reference](session-registry.md) - The recommended approach for most workflows
- [Session Registry: Best Practices](session-registry.md#best-practices) - When to use registry vs. manual
- [Migration Guide](session-registry.md#migration-guide) - Transition from manual to registry approach

View File

@@ -0,0 +1,503 @@
# Session Lifecycle Guide
> Practical guide for managing tmux sessions through their complete lifecycle - from creation to cleanup
## Overview
Tmux sessions managed by this skill follow a predictable lifecycle. Understanding these stages helps you make better decisions about when to create, reuse, or clean up sessions. This guide provides real-world workflows and decision trees for daily use.
**When to use this guide:**
- You're unsure whether to create a new session or reuse an existing one
- You need to debug a crashed or stuck session
- You want to understand proper cleanup workflows
- You're managing multiple parallel sessions
## Lifecycle Stages
```
┌─────────────┐
│ PRE-CHECK │ Check existing sessions
│ (optional) │ Tool: list-sessions.sh
└──────┬──────┘
┌─────────────┐
│ CREATE │ Spawn new tmux session
│ │ Tool: create-session.sh
└──────┬──────┘
┌─────────────┐
│ INIT │ Wait for startup (prompt appears)
│ │ Tool: wait-for-text.sh
└──────┬──────┘
┌─────────────┐
│ ACTIVE USE │ Send commands, capture output
│ │ Tools: safe-send.sh, wait-for-text.sh
└──────┬──────┘
┌─────────────┐
│ IDLE │ Session exists but unused
│ │ Tool: list-sessions.sh (check status)
└──────┬──────┘
┌─────────────┐
│ CLEANUP │ Remove dead/stale sessions
│ │ Tools: cleanup-sessions.sh, tmux kill-session
└─────────────┘
```
**Error recovery:**
- If session crashes → Check with `pane-health.sh`
- If session becomes zombie → Remove with `cleanup-sessions.sh`
- If network interruption → Reconnect using `tmux attach` or resume with session name
## Common Workflows
### 1. Quick Python Calculation (Ephemeral Session)
**Use case:** Run a one-off calculation, get the result, and cleanup immediately.
**Duration:** < 5 minutes
**Commands:**
```bash
# 1. Check no conflicts
./tools/list-sessions.sh
# 2. Create session
./tools/create-session.sh -n quick-calc --python
# 3. Send calculation
./tools/safe-send.sh -s quick-calc -c "import math; print(math.factorial(100))" -w ">>>" -T 10
# 4. Capture output
SOCKET=$(jq -r '.sessions["quick-calc"].socket' ~/.local/state/claude-tmux/.sessions.json)
tmux -S "$SOCKET" capture-pane -p -t quick-calc:0.0 -S -10
# 5. Cleanup immediately
tmux -S "$SOCKET" kill-session -t quick-calc
./tools/cleanup-sessions.sh
```
**When to use:** One-time calculations, quick tests, disposable experiments.
### 2. Long-Running Analysis (Persistent Session)
**Use case:** Hours or days of interactive REPL work with state preservation.
**Duration:** Hours to days
**Commands:**
```bash
# 1. Check existing sessions first
./tools/list-sessions.sh
# 2. Create persistent session with descriptive name
./tools/create-session.sh -n data-analysis-2025-11 --python
# 3. Load data (may take time)
./tools/safe-send.sh -s data-analysis-2025-11 -c "import pandas as pd" -w ">>>"
./tools/safe-send.sh -s data-analysis-2025-11 -c "df = pd.read_csv('large_dataset.csv')" -w ">>>" -T 300
# 4. Work interactively over time
./tools/safe-send.sh -s data-analysis-2025-11 -c "df.describe()" -w ">>>"
# ... hours later ...
./tools/safe-send.sh -s data-analysis-2025-11 -c "df.groupby('category').mean()" -w ">>>"
# 5. Check session health periodically
./tools/pane-health.sh -s data-analysis-2025-11
# 6. Cleanup only when done (days later)
./tools/list-sessions.sh # Verify it's the right session
tmux -S "$(jq -r '.sessions["data-analysis-2025-11"].socket' ~/.local/state/claude-tmux/.sessions.json)" kill-session -t data-analysis-2025-11
./tools/cleanup-sessions.sh
```
**When to use:** Long-running analysis, state preservation, multiple work sessions.
**Best practices:**
- Use descriptive names (include date or project name)
- Check health before resuming work
- Don't cleanup until completely done
- Capture important output early
### 3. Recovering from Crashed Session
**Use case:** Session died unexpectedly, need to investigate and restart.
**Duration:** 5-10 minutes
**Commands:**
```bash
# 1. Check session status
./tools/list-sessions.sh
# Output shows: status "dead" or "zombie"
# 2. Diagnose the issue
./tools/pane-health.sh -s crashed-session --format text
# Check exit code: 1=dead, 2=missing, 3=zombie
# 3. Capture any remaining output for debugging
SOCKET=$(jq -r '.sessions["crashed-session"].socket' ~/.local/state/claude-tmux/.sessions.json)
tmux -S "$SOCKET" capture-pane -p -t crashed-session:0.0 -S -200 > crash-output.txt 2>/dev/null || echo "Pane unavailable"
# 4. Remove dead session from registry
./tools/cleanup-sessions.sh # Removes dead sessions automatically
# 5. Create replacement session
./tools/create-session.sh -n crashed-session-recovery --python
# 6. Resume work with new session
./tools/safe-send.sh -s crashed-session-recovery -c "# Resuming from crash..." -w ">>>"
```
**When to use:** Session crashed, process died, debugging needed.
**Troubleshooting tips:**
- Always capture output before cleanup (may contain error messages)
- Check system logs if crash is mysterious: `dmesg`, `journalctl`
- Verify PYTHON_BASIC_REPL=1 was set (common cause of silent failures)
### 4. Multi-Session Workspace (Parallel Tasks)
**Use case:** Multiple parallel tasks (data loading, analysis, monitoring) in separate sessions.
**Duration:** Variable
**Commands:**
```bash
# 1. Check existing sessions
./tools/list-sessions.sh
# 2. Create multiple sessions with clear names
./tools/create-session.sh -n loader-session --python
./tools/create-session.sh -n analysis-session --python
./tools/create-session.sh -n monitor-session --python
# 3. Start parallel work
# Session 1: Load large dataset
./tools/safe-send.sh -s loader-session -c "import pandas as pd; df = pd.read_csv('huge.csv')" -w ">>>" -T 600 &
# Session 2: Run analysis on existing data
./tools/safe-send.sh -s analysis-session -c "results = analyze_data()" -w ">>>" -T 300 &
# Session 3: Monitor system
./tools/safe-send.sh -s monitor-session -c "import psutil; psutil.cpu_percent()" -w ">>>"
# 4. Check all sessions status
./tools/list-sessions.sh
# 5. Cleanup when done
./tools/cleanup-sessions.sh --all # Or cleanup individually
```
**When to use:** Parallel independent tasks, workspace organization, resource isolation.
**Best practices:**
- Use descriptive session names (purpose-based: loader-*, analysis-*, monitor-*)
- One session per major task (don't overload single session)
- Check `list-sessions.sh` frequently to monitor all sessions
- Cleanup all related sessions together when project complete
## Decision Trees
### Should I Create a New Session or Reuse?
```
Start
├─→ Is this a quick one-off task?
│ └─→ YES → Create new ephemeral session
│ (cleanup immediately after)
├─→ Do I have an existing session for this work?
│ ├─→ YES → Is it still alive?
│ │ ├─→ YES → Reuse existing session
│ │ └─→ NO → Create new session
│ └─→ NO → Create new session
└─→ Am I starting a new long-running project?
└─→ YES → Create new persistent session
(descriptive name with date/project)
```
**Rules of thumb:**
-**Create new** for: Different projects, parallel tasks, isolation needed
-**Reuse** for: Continuing same work, state preservation needed
-**Don't reuse** for: Unrelated tasks (state pollution), different projects
### When Should I Clean Up Sessions?
```
Start
├─→ Is session marked as "dead" or "zombie"?
│ └─→ YES → Cleanup immediately
│ (./tools/cleanup-sessions.sh)
├─→ Has the task completed?
│ └─→ YES → Cleanup immediately
│ (tmux kill-session + cleanup-sessions.sh)
├─→ Is session idle for > 1 hour?
│ ├─→ AND no state preservation needed?
│ │ └─→ YES → Cleanup
│ └─→ OR state needed?
│ └─→ NO → Keep (long-running analysis)
└─→ Am I done for the day but continuing tomorrow?
└─→ Keep session (resume later)
```
**Cleanup commands:**
```bash
# Remove only dead/zombie sessions (safe, default)
./tools/cleanup-sessions.sh
# Remove sessions older than 1 hour
./tools/cleanup-sessions.sh --older-than 1h
# Remove all sessions (careful!)
./tools/cleanup-sessions.sh --all
# Preview what would be removed (dry-run)
./tools/cleanup-sessions.sh --dry-run
```
### How Should I Handle Session Errors?
```
Error Detected
├─→ Is session responding to commands?
│ ├─→ NO → Check health (pane-health.sh)
│ │ ├─→ Dead/zombie → Capture output → Cleanup → Recreate
│ │ └─→ Alive but hung → Send C-c → Retry command
│ └─→ YES → Problem is elsewhere (not session)
├─→ Are commands being executed but output wrong?
│ └─→ Check PYTHON_BASIC_REPL=1 was set
│ └─→ Not set? Restart session with env var
└─→ Is prompt not appearing?
└─→ Increase timeout (-T flag)
└─→ Still failing? Check pane content manually
```
## Tool Reference Matrix
| Lifecycle Stage | Tool | Purpose | Example Command |
|-----------------|------|---------|-----------------|
| **Pre-check** | `list-sessions.sh` | Check existing sessions | `./tools/list-sessions.sh` |
| **Create** | `create-session.sh` | Spawn new session | `./tools/create-session.sh -n my-session --python` |
| **Init** | `wait-for-text.sh` | Wait for startup prompt | `./tools/wait-for-text.sh -s my-session -p ">>>" -T 15` |
| **Active Use** | `safe-send.sh` | Send commands safely | `./tools/safe-send.sh -s my-session -c "print(42)" -w ">>>"` |
| **Active Use** | `safe-send.sh` (multiline) | Send code blocks | `./tools/safe-send.sh -s my-session -m -c "def foo():\n return 42" -w ">>>"` |
| **Monitoring** | `pane-health.sh` | Check session health | `./tools/pane-health.sh -s my-session` |
| **Monitoring** | `list-sessions.sh` | View all sessions | `./tools/list-sessions.sh --json` |
| **Capture** | `tmux capture-pane` | Get session output | `tmux -S $SOCKET capture-pane -p -t session:0.0 -S -100` |
| **Cleanup** | `cleanup-sessions.sh` | Remove dead sessions | `./tools/cleanup-sessions.sh` |
| **Cleanup** | `tmux kill-session` | Terminate specific session | `tmux -S $SOCKET kill-session -t my-session` |
## Troubleshooting Quick Reference
### Session Not Found
**Symptom:** `./tools/safe-send.sh -s my-session -c "cmd"` fails with "Session 'my-session' not found in registry"
**Causes:**
1. Session was never created
2. Session was created but not registered (`--no-register` flag used)
3. Session was cleaned up by mistake
**Fixes:**
```bash
# Check what sessions exist
./tools/list-sessions.sh
# Check if session exists outside registry
./tools/find-sessions.sh --all
# Recreate session
./tools/create-session.sh -n my-session --python
```
### Commands Not Executing
**Symptom:** Commands sent but REPL shows no activity
**Causes:**
1. PYTHON_BASIC_REPL=1 not set (most common!)
2. Session crashed/dead
3. Prompt not detected correctly
**Fixes:**
```bash
# Check session health
./tools/pane-health.sh -s my-session --format text
# Manually check what's in the pane
SOCKET=$(jq -r '.sessions["my-session"].socket' ~/.local/state/claude-tmux/.sessions.json)
tmux -S "$SOCKET" capture-pane -p -t my-session:0.0 -S -20
# If Python REPL, verify PYTHON_BASIC_REPL=1 was set
# Symptom: You'll see fancy colored prompts instead of plain >>>
# Fix: Kill and recreate with correct env var
```
### Output Capture Issues
**Symptom:** `tmux capture-pane` returns incomplete or truncated output
**Causes:**
1. Pane history buffer too small
2. Lines wrapped due to terminal width
3. ANSI color codes interfering
**Fixes:**
```bash
# Capture more history (-S flag = start line)
tmux -S "$SOCKET" capture-pane -p -t session:0.0 -S -500
# Join wrapped lines (-J flag)
tmux -S "$SOCKET" capture-pane -p -J -t session:0.0 -S -200
# Strip ANSI color codes
tmux -S "$SOCKET" capture-pane -p -t session:0.0 | sed 's/\x1b\[[0-9;]*[a-zA-Z]//g'
```
### Dead/Zombie Sessions
**Symptom:** `./tools/list-sessions.sh` shows status "dead" or "zombie"
**Causes:**
1. Process crashed
2. Session was killed manually
3. Out of memory / system issue
**Fixes:**
```bash
# Capture any remaining output for debugging
SOCKET=$(jq -r '.sessions["dead-session"].socket' ~/.local/state/claude-tmux/.sessions.json)
tmux -S "$SOCKET" capture-pane -p -t dead-session:0.0 -S -500 > debug.txt 2>/dev/null
# Remove from registry
./tools/cleanup-sessions.sh
# Recreate if needed
./tools/create-session.sh -n replacement-session --python
```
### Multiple Sessions with Same Name
**Symptom:** `./tools/create-session.sh -n my-session --python` fails with "session already exists"
**Causes:**
1. Forgot to check existing sessions first
2. Previous session not cleaned up
**Fixes:**
```bash
# Check existing sessions
./tools/list-sessions.sh
# Use different name OR cleanup old session first
tmux -S "$SOCKET" kill-session -t my-session
./tools/cleanup-sessions.sh
# Then create new session
./tools/create-session.sh -n my-session --python
```
## Best Practices
### ✅ DO
1. **Always check before creating**
```bash
./tools/list-sessions.sh # Check for conflicts first
```
2. **Use descriptive session names**
```bash
# Good: data-analysis-2025-11, debug-api-auth, monitor-prod
# Bad: session1, test, tmp
```
3. **Set PYTHON_BASIC_REPL=1 for Python**
```bash
# This is handled by create-session.sh --python automatically
# But verify if creating manually
```
4. **Wait for prompts after every command**
```bash
./tools/safe-send.sh -s session -c "command" -w ">>>" -T 10
# Always include -w flag for synchronization
```
5. **Use session registry (-s flag)**
```bash
# Preferred: ./tools/safe-send.sh -s my-session -c "cmd"
# Avoid: ./tools/safe-send.sh -S $SOCKET -t session:0.0 -c "cmd"
```
6. **Check health before critical operations**
```bash
if ./tools/pane-health.sh -s session; then
./tools/safe-send.sh -s session -c "critical_command"
fi
```
7. **Use multiline mode for code blocks**
```bash
# 10x faster than line-by-line
./tools/safe-send.sh -s session -m -c "def foo():\n return 42" -w ">>>"
```
8. **Cleanup dead sessions regularly**
```bash
# Run periodically or in cleanup scripts
./tools/cleanup-sessions.sh
```
9. **Capture output early and often**
```bash
# Before making destructive changes
tmux -S "$SOCKET" capture-pane -p -t session:0.0 -S -200 > backup.txt
```
10. **Use --dry-run for cleanup preview**
```bash
# See what would be removed before executing
./tools/cleanup-sessions.sh --dry-run
./tools/cleanup-sessions.sh --older-than 2d --dry-run
```
### ❌ DON'T
1. **Don't create sessions without checking first** - Leads to name conflicts
2. **Don't reuse sessions for unrelated work** - State pollution causes bugs
3. **Don't forget -w flag when sending commands** - Race conditions
4. **Don't skip health checks before critical operations** - Pane might be dead
5. **Don't use generic session names** - Hard to manage multiple sessions
6. **Don't leave dead sessions in registry** - Clutters output, wastes resources
7. **Don't forget to cleanup ephemeral sessions** - Resource leaks
8. **Don't send commands too fast** - Wait for prompts between commands
9. **Don't ignore session health warnings** - Investigate issues early
10. **Don't use line-by-line for large code blocks** - Use multiline mode instead
## Related References
- [Session Registry Guide](./session-registry.md) - Deep dive on registry system
- [Direct Socket Control](./direct-socket-control.md) - Advanced manual socket management
- [Main SKILL.md](../SKILL.md) - Complete tmux skill documentation
---
**Version:** Documented for tmux skill v1.3.0+ (includes multiline support)

File diff suppressed because it is too large Load Diff

263
tools/cleanup-sessions.sh Executable file
View File

@@ -0,0 +1,263 @@
#!/usr/bin/env bash
#
# cleanup-sessions.sh - Clean up dead or old tmux sessions
#
# Removes dead sessions from the registry or optionally cleans up
# all sessions or sessions older than a specified threshold.
#
# Usage:
# ./cleanup-sessions.sh [options]
#
# Options:
# --dry-run Show what would be cleaned without doing it
# --all Remove all sessions (even alive ones)
# --older-than Remove sessions older than duration (e.g., "1h", "2d")
# -h, --help Show this help message
#
# Exit codes:
# 0 - Success
# 1 - Invalid arguments
set -euo pipefail
# Get script directory to source registry library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/registry.sh
source "$SCRIPT_DIR/lib/registry.sh"
#------------------------------------------------------------------------------
# Configuration
#------------------------------------------------------------------------------
dry_run=false
clean_all=false
older_than=""
#------------------------------------------------------------------------------
# Functions
#------------------------------------------------------------------------------
usage() {
cat << EOF
Usage: $(basename "$0") [options]
Clean up dead or old tmux sessions from the registry.
Options:
--dry-run Show what would be cleaned without actually removing
--all Remove all sessions (even alive ones)
--older-than DUR Remove sessions older than duration
-h, --help Show this help message
Duration Format:
Supported units: s (seconds), m (minutes), h (hours), d (days)
Examples: 30m, 2h, 1d, 3600s
Cleanup Modes:
Default: Remove only dead/missing/zombie sessions
--all: Remove all sessions regardless of health
--older-than: Remove sessions older than specified duration
Examples:
# Show what dead sessions would be removed (dry-run)
$(basename "$0") --dry-run
# Remove all dead sessions
$(basename "$0")
# Remove sessions inactive for more than 1 hour
$(basename "$0") --older-than 1h
# Remove all sessions (even alive ones)
$(basename "$0") --all
# Dry-run: show sessions older than 2 days
$(basename "$0") --dry-run --older-than 2d
Exit codes:
0 - Success
1 - Invalid arguments
EOF
}
# Parse duration string to seconds
# Args: duration string (e.g., "1h", "30m", "2d")
# Returns: seconds as integer
parse_duration() {
local dur="$1"
if [[ "$dur" =~ ^([0-9]+)([smhd])$ ]]; then
local value="${BASH_REMATCH[1]}"
local unit="${BASH_REMATCH[2]}"
case "$unit" in
s) echo "$value" ;;
m) echo "$((value * 60))" ;;
h) echo "$((value * 3600))" ;;
d) echo "$((value * 86400))" ;;
esac
else
echo "Error: Invalid duration format: $dur" >&2
echo "Use format like: 30m, 2h, 1d" >&2
return 1
fi
}
# Check if session is older than threshold
# Args: created_at timestamp, threshold in seconds
# Returns: 0 if older, 1 if newer
is_older_than() {
local created="$1"
local threshold_secs="$2"
# Convert ISO8601 to epoch (cross-platform)
local created_epoch
created_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$created" "+%s" 2>/dev/null || \
date -d "$created" "+%s" 2>/dev/null || echo "0")
if [[ "$created_epoch" == "0" ]]; then
# Can't parse date, assume it's old
return 0
fi
local now_epoch
now_epoch=$(date "+%s")
local age=$((now_epoch - created_epoch))
[[ $age -gt $threshold_secs ]]
}
#------------------------------------------------------------------------------
# Argument parsing
#------------------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
dry_run=true
shift
;;
--all)
clean_all=true
shift
;;
--older-than)
older_than="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Error: Unknown option: $1" >&2
usage
exit 1
;;
esac
done
#------------------------------------------------------------------------------
# Validate arguments
#------------------------------------------------------------------------------
threshold_secs=0
if [[ -n "$older_than" ]]; then
threshold_secs=$(parse_duration "$older_than") || exit 1
fi
#------------------------------------------------------------------------------
# Get sessions from registry
#------------------------------------------------------------------------------
registry_data=$(registry_list_sessions)
session_names=$(echo "$registry_data" | jq -r '.sessions | keys[]' 2>/dev/null || echo "")
if [[ -z "$session_names" ]]; then
echo "No sessions in registry."
exit 0
fi
#------------------------------------------------------------------------------
# Find sessions to remove
#------------------------------------------------------------------------------
# Path to pane-health tool
PANE_HEALTH="$SCRIPT_DIR/pane-health.sh"
sessions_to_remove=()
removed_count=0
while IFS= read -r name; do
[[ -z "$name" ]] && continue
# Get session data
socket=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].socket")
target=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].target")
created=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].created_at")
# Determine if session should be removed
should_remove=false
reason=""
if [[ "$clean_all" == true ]]; then
should_remove=true
reason="all sessions mode"
else
# Check health status
if [[ -x "$PANE_HEALTH" ]]; then
if ! "$PANE_HEALTH" -S "$socket" -t "$target" --format text >/dev/null 2>&1; then
should_remove=true
reason="dead/missing/zombie"
fi
fi
# Check age if threshold specified
if [[ -n "$older_than" ]] && [[ "$threshold_secs" -gt 0 ]]; then
if is_older_than "$created" "$threshold_secs"; then
should_remove=true
if [[ -n "$reason" ]]; then
reason="$reason + older than $older_than"
else
reason="older than $older_than"
fi
fi
fi
fi
# Add to removal list if needed
if [[ "$should_remove" == true ]]; then
sessions_to_remove+=("$name|$reason")
fi
done <<< "$session_names"
#------------------------------------------------------------------------------
# Remove sessions
#------------------------------------------------------------------------------
if [[ ${#sessions_to_remove[@]} -eq 0 ]]; then
echo "No sessions to clean up."
exit 0
fi
if [[ "$dry_run" == true ]]; then
echo "Dry-run mode: Would remove ${#sessions_to_remove[@]} session(s):"
for session_info in "${sessions_to_remove[@]}"; do
IFS='|' read -r name reason <<< "$session_info"
echo " - $name ($reason)"
done
else
echo "Removing ${#sessions_to_remove[@]} session(s):"
for session_info in "${sessions_to_remove[@]}"; do
IFS='|' read -r name reason <<< "$session_info"
echo " - $name ($reason)"
if registry_remove_session "$name"; then
removed_count=$((removed_count + 1))
else
echo " Warning: Failed to remove $name" >&2
fi
done
echo "Removed $removed_count session(s) successfully."
fi
exit 0

224
tools/create-session.sh Executable file
View File

@@ -0,0 +1,224 @@
#!/usr/bin/env bash
#
# create-session.sh - Create and register tmux sessions
#
# Creates a new tmux session and optionally registers it in the session registry.
# Supports launching different types of sessions (Python REPL, gdb, shell).
#
# Usage:
# ./create-session.sh -n <name> [options]
#
# Options:
# -n, --name Session name (required)
# -S, --socket Custom socket path (optional, uses default)
# -w, --window Window name (default: "shell")
# --python Launch Python REPL with PYTHON_BASIC_REPL=1
# --gdb Launch gdb
# --shell Launch shell (default)
# --no-register Don't add to registry
# -h, --help Show this help message
#
# Exit codes:
# 0 - Success
# 1 - Invalid arguments
# 2 - Session already exists
# 3 - Tmux command failed
# 4 - Registry operation failed
set -euo pipefail
# Get script directory to source registry library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/registry.sh
source "$SCRIPT_DIR/lib/registry.sh"
#------------------------------------------------------------------------------
# Configuration
#------------------------------------------------------------------------------
# Defaults
session_name=""
socket=""
window_name="shell"
session_type="shell"
launch_command="bash"
register_session=true
#------------------------------------------------------------------------------
# Functions
#------------------------------------------------------------------------------
usage() {
cat << EOF
Usage: $(basename "$0") -n <name> [options]
Create a new tmux session and optionally register it in the session registry.
Options:
-n, --name Session name (required)
-S, --socket Custom socket path (optional, uses default)
-w, --window Window name (default: "shell")
--python Launch Python REPL with PYTHON_BASIC_REPL=1
--gdb Launch gdb
--shell Launch shell (default)
--no-register Don't add to registry
-h, --help Show this help message
Session Types:
--shell Launches bash (default)
--python Launches Python REPL with PYTHON_BASIC_REPL=1
--gdb Launches gdb debugger
Examples:
# Create Python REPL session (auto-registered)
$(basename "$0") -n my-python --python
# Create session with custom socket
$(basename "$0") -n my-session -S /tmp/custom.sock --shell
# Create session without registering
$(basename "$0") -n temp-session --no-register
Exit codes:
0 - Success
1 - Invalid arguments
2 - Session already exists
3 - Tmux command failed
4 - Registry operation failed
EOF
}
#------------------------------------------------------------------------------
# Argument parsing
#------------------------------------------------------------------------------
if [[ $# -eq 0 ]]; then
usage
exit 1
fi
while [[ $# -gt 0 ]]; do
case "$1" in
-n|--name)
session_name="$2"
shift 2
;;
-S|--socket)
socket="$2"
shift 2
;;
-w|--window)
window_name="$2"
shift 2
;;
--python)
session_type="python-repl"
launch_command="PYTHON_BASIC_REPL=1 python3 -q"
shift
;;
--gdb)
session_type="debugger"
launch_command="gdb"
shift
;;
--shell)
session_type="shell"
launch_command="bash"
shift
;;
--no-register)
register_session=false
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Error: Unknown option: $1" >&2
usage
exit 1
;;
esac
done
#------------------------------------------------------------------------------
# Validation
#------------------------------------------------------------------------------
if [[ -z "$session_name" ]]; then
echo "Error: Session name is required (use -n <name>)" >&2
usage
exit 1
fi
# Set default socket if not provided
if [[ -z "$socket" ]]; then
socket="$CLAUDE_TMUX_SOCKET_DIR/claude.sock"
fi
# Ensure socket directory exists
socket_dir="$(dirname "$socket")"
mkdir -p "$socket_dir"
#------------------------------------------------------------------------------
# Check if session already exists
#------------------------------------------------------------------------------
# Check in registry first
if [[ "$register_session" == true ]] && registry_session_exists "$session_name"; then
echo "Error: Session '$session_name' already exists in registry" >&2
echo "Use a different name or remove the existing session first" >&2
exit 2
fi
# Check if tmux session actually exists on this socket
if tmux -S "$socket" has-session -t "$session_name" 2>/dev/null; then
echo "Error: Tmux session '$session_name' already exists on socket $socket" >&2
echo "Use a different name or kill the existing session first" >&2
exit 2
fi
#------------------------------------------------------------------------------
# Create tmux session
#------------------------------------------------------------------------------
if ! tmux -S "$socket" new-session -d -s "$session_name" -n "$window_name" "$launch_command" 2>/dev/null; then
echo "Error: Failed to create tmux session '$session_name'" >&2
exit 3
fi
# Get the session PID
session_pid=$(tmux -S "$socket" display-message -p -t "$session_name" '#{pane_pid}' 2>/dev/null || echo "")
# Build target (session:window.pane)
target="$session_name:0.0"
#------------------------------------------------------------------------------
# Register session
#------------------------------------------------------------------------------
if [[ "$register_session" == true ]]; then
if ! registry_add_session "$session_name" "$socket" "$target" "$session_type" "$session_pid"; then
echo "Warning: Session created but failed to register in registry" >&2
# Don't fail completely, session was created successfully
fi
fi
#------------------------------------------------------------------------------
# Output session info as JSON
#------------------------------------------------------------------------------
cat << EOF
{
"name": "$session_name",
"socket": "$socket",
"target": "$target",
"type": "$session_type",
"pid": ${session_pid:-null},
"window": "$window_name",
"registered": $register_session
}
EOF
exit 0

262
tools/find-sessions.sh Executable file
View File

@@ -0,0 +1,262 @@
#!/usr/bin/env bash
#
# find-sessions.sh - Discover and list tmux sessions on sockets
#
# PURPOSE:
# Find and display information about tmux sessions, either on a specific
# socket or by scanning all sockets in a directory. Useful for discovering
# what agent sessions are currently running.
#
# HOW IT WORKS:
# 1. Identify target socket(s) based on command-line options
# 2. Query each socket for running tmux sessions
# 3. Display session info: name, attach status, creation time
# 4. Optionally filter by session name substring
#
# USE CASES:
# - List all agent sessions across multiple sockets
# - Find a specific session by name (partial matching)
# - Check if a session is attached or detached
# - See when sessions were created
# - Enumerate sessions before cleanup
#
# EXAMPLES:
# # List sessions on default tmux socket
# ./find-sessions.sh
#
# # List sessions on specific socket by name
# ./find-sessions.sh -L mysocket
#
# # List sessions on specific socket by path
# ./find-sessions.sh -S /tmp/claude-tmux-sockets/claude.sock
#
# # Scan all sockets in directory
# ./find-sessions.sh --all
#
# # Find sessions with "python" in the name
# ./find-sessions.sh --all -q python
#
# DEPENDENCIES:
# - bash (with arrays, [[, functions)
# - tmux (for list-sessions)
# - grep (for filtering by query)
#
# Bash strict mode:
# -e: Exit immediately if any command fails
# -u: Treat unset variables as errors
# -o pipefail: Pipe fails if any command in pipeline fails
set -euo pipefail
usage() {
cat <<'USAGE'
Usage: find-sessions.sh [-L socket-name|-S socket-path|-A] [-q pattern]
List tmux sessions on a socket (default tmux socket if none provided).
Options:
-L, --socket tmux socket name (passed to tmux -L)
-S, --socket-path tmux socket path (passed to tmux -S)
-A, --all scan all sockets under CLAUDE_TMUX_SOCKET_DIR
-q, --query case-insensitive substring to filter session names
-h, --help show this help
USAGE
}
# ============================================================================
# Default Configuration
# ============================================================================
# Socket specification (mutually exclusive)
socket_name="" # tmux socket name (for tmux -L)
socket_path="" # tmux socket path (for tmux -S)
# Filtering and scanning options
query="" # substring to filter session names (case-insensitive)
scan_all=false # whether to scan all sockets in socket_dir
# Directory containing agent tmux sockets
# Priority: CLAUDE_TMUX_SOCKET_DIR env var > TMPDIR/claude-tmux-sockets > /tmp/claude-tmux-sockets
socket_dir="${CLAUDE_TMUX_SOCKET_DIR:-${TMPDIR:-/tmp}/claude-tmux-sockets}"
# ============================================================================
# Parse Command-Line Arguments
# ============================================================================
while [[ $# -gt 0 ]]; do
case "$1" in
-L|--socket) socket_name="${2-}"; shift 2 ;; # Socket name mode
-S|--socket-path) socket_path="${2-}"; shift 2 ;; # Socket path mode
-A|--all) scan_all=true; shift ;; # Scan all mode
-q|--query) query="${2-}"; shift 2 ;; # Filter by name
-h|--help) usage; exit 0 ;; # Show help
*) echo "Unknown option: $1" >&2; usage; exit 1 ;; # Error on unknown
esac
done
# ============================================================================
# Validate Options
# ============================================================================
# Cannot use --all with specific socket options (they're mutually exclusive)
if [[ "$scan_all" == true && ( -n "$socket_name" || -n "$socket_path" ) ]]; then
echo "Cannot combine --all with -L or -S" >&2
exit 1
fi
# Cannot use both -L and -S at the same time (different socket types)
if [[ -n "$socket_name" && -n "$socket_path" ]]; then
echo "Use either -L or -S, not both" >&2
exit 1
fi
# Check that tmux is installed and available in PATH
if ! command -v tmux >/dev/null 2>&1; then
echo "tmux not found in PATH" >&2
exit 1
fi
# ============================================================================
# Function: list_sessions
# ============================================================================
# Query a tmux socket for sessions and display formatted output
#
# Arguments:
# $1: Label describing the socket (for display purposes)
# $@: Remaining args are passed to tmux command (e.g., -L name or -S path)
#
# Returns:
# 0 if sessions found (or no sessions after filtering)
# 1 if tmux server not running on this socket
#
# Output format:
# Sessions on <label>:
# - session-name (attached|detached, started <timestamp>)
#
list_sessions() {
# Store label for display, then shift to get remaining args
local label="$1"; shift
# Build tmux command array with remaining args (socket options)
local tmux_cmd=(tmux "$@")
# --------------------------------------------------------------------------
# Query tmux for session information
# --------------------------------------------------------------------------
# tmux list-sessions -F specifies output format:
# #{session_name}: Name of the session
# #{session_attached}: 1 if attached, 0 if detached
# #{session_created_string}: Human-readable creation timestamp
# Tab-separated output for easy parsing
# 2>/dev/null: Suppress errors if no server running
# if !: Check if command failed (no server = exit code 1)
#
if ! sessions="$("${tmux_cmd[@]}" list-sessions -F '#{session_name}\t#{session_attached}\t#{session_created_string}' 2>/dev/null)"; then
echo "No tmux server found on $label" >&2
return 1
fi
# --------------------------------------------------------------------------
# Filter sessions by query if provided
# --------------------------------------------------------------------------
# -i: Case-insensitive search
# --: End of options (allows query starting with -)
# || true: Don't fail if grep finds no matches (returns exit 1)
#
if [[ -n "$query" ]]; then
sessions="$(printf '%s\n' "$sessions" | grep -i -- "$query" || true)"
fi
# --------------------------------------------------------------------------
# Handle case where no sessions match query
# --------------------------------------------------------------------------
if [[ -z "$sessions" ]]; then
echo "No sessions found on $label"
return 0
fi
# --------------------------------------------------------------------------
# Format and display session information
# --------------------------------------------------------------------------
echo "Sessions on $label:"
# Parse tab-separated values into name, attached, created
# IFS=$'\t': Set field separator to tab character
# read -r: Don't interpret backslashes (raw input)
printf '%s\n' "$sessions" | while IFS=$'\t' read -r name attached created; do
# Convert attached flag (1/0) to human-readable label
attached_label=$([[ "$attached" == "1" ]] && echo "attached" || echo "detached")
# Display formatted session info
printf ' - %s (%s, started %s)\n' "$name" "$attached_label" "$created"
done
}
# ============================================================================
# Main Execution: Scan All Mode
# ============================================================================
# Scan all socket files in socket_dir and list sessions on each
if [[ "$scan_all" == true ]]; then
# Verify socket directory exists
if [[ ! -d "$socket_dir" ]]; then
echo "Socket directory not found: $socket_dir" >&2
exit 1
fi
# --------------------------------------------------------------------------
# Enumerate all files in socket directory
# --------------------------------------------------------------------------
# shopt -s nullglob: If no matches, glob expands to empty array (not literal *)
# This prevents errors when directory is empty
shopt -s nullglob
sockets=("$socket_dir"/*)
shopt -u nullglob # Restore default behavior
# Check if any files were found
if [[ "${#sockets[@]}" -eq 0 ]]; then
echo "No sockets found under $socket_dir" >&2
exit 1
fi
# --------------------------------------------------------------------------
# Iterate through all socket files and list sessions
# --------------------------------------------------------------------------
# Track exit code: 0 = all succeeded, 1 = at least one failed
exit_code=0
for sock in "${sockets[@]}"; do
# -S test: Check if file is a socket (not a regular file or directory)
# Skip non-socket files (e.g., .DS_Store, temp files)
if [[ ! -S "$sock" ]]; then
continue
fi
# Call list_sessions for this socket
# || exit_code=$?: Capture failure exit code but continue loop
list_sessions "socket path '$sock'" -S "$sock" || exit_code=$?
done
# Exit with captured exit code (0 if all succeeded, 1 if any failed)
exit "$exit_code"
fi
# ============================================================================
# Main Execution: Single Socket Mode
# ============================================================================
# List sessions on a specific socket (or default socket)
# Start with base tmux command
tmux_cmd=(tmux)
socket_label="default socket"
# Add socket-specific options based on user input
if [[ -n "$socket_name" ]]; then
# -L mode: Named socket (e.g., tmux -L mysocket)
tmux_cmd+=(-L "$socket_name")
socket_label="socket name '$socket_name'"
elif [[ -n "$socket_path" ]]; then
# -S mode: Socket path (e.g., tmux -S /tmp/my.sock)
tmux_cmd+=(-S "$socket_path")
socket_label="socket path '$socket_path'"
fi
# If neither set, use default tmux socket (no additional flags)
# Call list_sessions with constructed command
# ${tmux_cmd[@]:1}: Array slice starting at index 1 (skips "tmux" itself)
# This passes only the flags (e.g., "-L mysocket" or "-S /path")
list_sessions "$socket_label" "${tmux_cmd[@]:1}"

308
tools/kill-session.sh Executable file
View File

@@ -0,0 +1,308 @@
#!/usr/bin/env bash
#
# kill-session.sh - Kill tmux session and remove from registry
#
# PURPOSE:
# Atomically kill a tmux session and remove it from the session registry.
# Provides a single operation to fully clean up a session.
#
# USAGE:
# ./kill-session.sh [options]
#
# OPTIONS:
# -s, --session NAME Session name (uses registry lookup)
# -S, --socket PATH Socket path (explicit mode, requires -t)
# -t, --target TARGET Target pane (explicit mode, requires -S)
# --dry-run Show what would be done without executing
# -v, --verbose Verbose output
# -h, --help Show this help message
#
# EXIT CODES:
# 0 - Complete success (tmux session killed AND deregistered)
# 1 - Partial success (one operation succeeded, one failed)
# 2 - Complete failure (both operations failed or session not found)
# 3 - Invalid arguments
#
# EXAMPLES:
# # Kill session by name (registry lookup)
# ./kill-session.sh -s claude-python
#
# # Kill with explicit socket/target
# ./kill-session.sh -S /tmp/claude.sock -t my-session:0.0
#
# # Dry-run to see what would happen
# ./kill-session.sh -s claude-python --dry-run
#
# # Auto-detect single session
# ./kill-session.sh
#
# DEPENDENCIES:
# - bash, tmux, jq
# - lib/registry.sh (for session registry operations)
#
set -euo pipefail
# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Source registry library
# shellcheck source=lib/registry.sh
source "$SCRIPT_DIR/lib/registry.sh"
#------------------------------------------------------------------------------
# Configuration
#------------------------------------------------------------------------------
session_name=""
socket=""
target=""
dry_run=false
verbose=false
#------------------------------------------------------------------------------
# Functions
#------------------------------------------------------------------------------
usage() {
cat <<'EOF'
Usage: kill-session.sh [options]
Kill tmux session and remove from registry (atomic operation).
Options:
-s, --session NAME Session name (uses registry lookup)
-S, --socket PATH Socket path (explicit mode, requires -t)
-t, --target TARGET Target pane (explicit mode, requires -S)
--dry-run Show what would be done without executing
-v, --verbose Verbose output
-h, --help Show this help message
Exit codes:
0 - Complete success (killed AND deregistered)
1 - Partial success (one operation succeeded)
2 - Complete failure (both failed or not found)
3 - Invalid arguments
Examples:
# Kill session by name
kill-session.sh -s claude-python
# Kill with explicit socket/target
kill-session.sh -S /tmp/claude.sock -t session:0.0
# Dry-run
kill-session.sh -s claude-python --dry-run
# Auto-detect (if only one session exists)
kill-session.sh
Priority order (if multiple methods specified):
1. Explicit -S and -t (highest priority)
2. Session name -s (registry lookup)
3. Auto-detect (if no flags and only one session exists)
EOF
}
log_verbose() {
if [[ "$verbose" == true ]]; then
echo "$@" >&2
fi
}
# Auto-detect session if only one exists in registry
# Sets session_name global variable
# Returns: 0 if detected, 1 if cannot auto-detect
auto_detect_session() {
local registry_data session_count session_names
registry_data=$(registry_list_sessions)
session_names=$(echo "$registry_data" | jq -r '.sessions | keys[]' 2>/dev/null || echo "")
if [[ -z "$session_names" ]]; then
echo "Error: No sessions in registry" >&2
return 1
fi
session_count=$(echo "$session_names" | wc -l | tr -d ' ')
if [[ "$session_count" -eq 1 ]]; then
session_name="$session_names"
log_verbose "Auto-detected session: $session_name"
return 0
else
echo "Error: Multiple sessions found, specify -s session-name:" >&2
# shellcheck disable=SC2001 # sed needed for adding prefix to multiple lines
echo "$session_names" | sed 's/^/ - /' >&2
return 1
fi
}
#------------------------------------------------------------------------------
# Argument parsing
#------------------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
-s|--session)
session_name="${2:-}"
if [[ -z "$session_name" ]]; then
echo "Error: -s requires a session name" >&2
exit 3
fi
shift 2
;;
-S|--socket)
socket="${2:-}"
if [[ -z "$socket" ]]; then
echo "Error: -S requires a socket path" >&2
exit 3
fi
shift 2
;;
-t|--target)
target="${2:-}"
if [[ -z "$target" ]]; then
echo "Error: -t requires a target pane" >&2
exit 3
fi
shift 2
;;
--dry-run)
dry_run=true
shift
;;
-v|--verbose)
verbose=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Error: Unknown option: $1" >&2
usage
exit 3
;;
esac
done
#------------------------------------------------------------------------------
# Validate arguments
#------------------------------------------------------------------------------
# Priority 1: Explicit socket/target mode
if [[ -n "$socket" || -n "$target" ]]; then
if [[ -z "$socket" || -z "$target" ]]; then
echo "Error: Both -S and -t must be specified together" >&2
exit 3
fi
log_verbose "Using explicit mode: socket=$socket, target=$target"
# Priority 2: Session name mode (registry lookup)
elif [[ -n "$session_name" ]]; then
log_verbose "Using registry mode: session=$session_name"
# Look up socket and target from registry
if ! session_data=$(registry_get_session "$session_name" 2>/dev/null); then
echo "Error: Session '$session_name' not found in registry" >&2
exit 2
fi
socket=$(echo "$session_data" | jq -r '.socket')
target=$(echo "$session_data" | jq -r '.target')
if [[ -z "$socket" || -z "$target" ]]; then
echo "Error: Invalid session data in registry for '$session_name'" >&2
exit 2
fi
log_verbose "Resolved from registry: socket=$socket, target=$target"
# Priority 3: Auto-detect mode
else
log_verbose "Attempting auto-detect..."
if ! auto_detect_session; then
exit 2
fi
# Look up socket and target
session_data=$(registry_get_session "$session_name")
socket=$(echo "$session_data" | jq -r '.socket')
target=$(echo "$session_data" | jq -r '.target')
log_verbose "Auto-detected: socket=$socket, target=$target"
fi
# Extract session name from target if not already set
if [[ -z "$session_name" ]]; then
# Target format is typically "session-name:0.0"
session_name="${target%%:*}"
fi
#------------------------------------------------------------------------------
# Execute kill operations
#------------------------------------------------------------------------------
if [[ "$dry_run" == true ]]; then
echo "Dry-run mode: Would perform the following operations:"
echo " 1. Kill tmux session: tmux -S \"$socket\" kill-session -t \"$session_name\""
if registry_session_exists "$session_name"; then
echo " 2. Remove from registry: $session_name"
else
echo " 2. Session not in registry, skip deregistration"
fi
exit 0
fi
#------------------------------------------------------------------------------
# Actual execution
#------------------------------------------------------------------------------
tmux_killed=false
registry_removed=false
# Step 1: Kill tmux session
echo "Killing tmux session: $session_name"
if tmux -S "$socket" kill-session -t "$session_name" 2>/dev/null; then
echo " ✓ Tmux session killed successfully"
tmux_killed=true
log_verbose "Tmux kill-session succeeded"
else
exit_code=$?
echo " ✗ Failed to kill tmux session (exit code: $exit_code)" >&2
log_verbose "Tmux kill-session failed, session may not exist"
fi
# Step 2: Remove from registry
if registry_session_exists "$session_name"; then
echo "Removing from registry: $session_name"
if registry_remove_session "$session_name"; then
echo " ✓ Removed from registry successfully"
registry_removed=true
log_verbose "Registry removal succeeded"
else
echo " ✗ Failed to remove from registry" >&2
log_verbose "Registry removal failed"
fi
else
log_verbose "Session not in registry, skipping deregistration"
# Not in registry is OK if we killed the tmux session
registry_removed=true
fi
#------------------------------------------------------------------------------
# Determine final exit code
#------------------------------------------------------------------------------
if [[ "$tmux_killed" == true && "$registry_removed" == true ]]; then
echo "Session '$session_name' fully removed"
exit 0
elif [[ "$tmux_killed" == true || "$registry_removed" == true ]]; then
echo "Warning: Partial removal of session '$session_name'" >&2
exit 1
else
echo "Error: Failed to remove session '$session_name'" >&2
exit 2
fi

255
tools/list-sessions.sh Executable file
View File

@@ -0,0 +1,255 @@
#!/usr/bin/env bash
#
# list-sessions.sh - List all registered tmux sessions
#
# Lists all sessions in the registry with health status information.
# Supports both human-readable table format and JSON output.
#
# Usage:
# ./list-sessions.sh [--json]
#
# Options:
# --json Output as JSON instead of table
# -h, --help Show this help message
#
# Exit codes:
# 0 - Success
# 1 - Error
set -euo pipefail
# Get script directory to source libraries
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/registry.sh
source "$SCRIPT_DIR/lib/registry.sh"
# shellcheck source=lib/time_utils.sh
source "$SCRIPT_DIR/lib/time_utils.sh"
# Path to pane-health tool
PANE_HEALTH="$SCRIPT_DIR/pane-health.sh"
#------------------------------------------------------------------------------
# Configuration
#------------------------------------------------------------------------------
output_format="table"
#------------------------------------------------------------------------------
# Functions
#------------------------------------------------------------------------------
usage() {
cat << EOF
Usage: $(basename "$0") [--json]
List all registered tmux sessions with health status.
Options:
--json Output as JSON instead of table format
-h, --help Show this help message
Output Formats:
Table (default):
NAME SOCKET TARGET STATUS PID CREATED
my-python claude.sock my-python:0.0 alive 1234 2h ago
my-gdb claude.sock my-gdb:0.0 dead - 1h ago
JSON (--json):
{
"sessions": [
{"name": "my-python", "socket": "...", "status": "alive", ...}
],
"total": 2,
"alive": 1,
"dead": 1
}
Health Status:
alive - Session is running and healthy
dead - Pane is marked as dead
missing - Session/pane not found
zombie - Process not running
server - Tmux server not running
Examples:
# List sessions in table format
$(basename "$0")
# List sessions as JSON
$(basename "$0") --json
Exit codes:
0 - Success
1 - Error
EOF
}
# Get health status for a session
# Returns: status string (alive, dead, missing, zombie, server)
get_health_status() {
local socket="$1"
local target="$2"
if [[ ! -x "$PANE_HEALTH" ]]; then
echo "unknown"
return
fi
# Call pane-health.sh and interpret exit code
if "$PANE_HEALTH" -S "$socket" -t "$target" --format text >/dev/null 2>&1; then
echo "alive"
else
local exit_code=$?
case $exit_code in
1) echo "dead" ;;
2) echo "missing" ;;
3) echo "zombie" ;;
4) echo "server" ;;
*) echo "unknown" ;;
esac
fi
}
#------------------------------------------------------------------------------
# Argument parsing
#------------------------------------------------------------------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--json)
output_format="json"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Error: Unknown option: $1" >&2
usage
exit 1
;;
esac
done
#------------------------------------------------------------------------------
# Get sessions from registry
#------------------------------------------------------------------------------
registry_data=$(registry_list_sessions)
session_names=$(echo "$registry_data" | jq -r '.sessions | keys[]' 2>/dev/null || echo "")
# Count sessions
total_count=0
alive_count=0
dead_count=0
# Build session list with health info
sessions_with_health=()
if [[ -n "$session_names" ]]; then
while IFS= read -r name; do
[[ -z "$name" ]] && continue
# Get session data
socket=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].socket")
target=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].target")
type=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].type")
pid=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].pid // \"\"")
created=$(echo "$registry_data" | jq -r ".sessions[\"$name\"].created_at")
# Get health status
status=$(get_health_status "$socket" "$target")
# Update counters
total_count=$((total_count + 1))
if [[ "$status" == "alive" ]]; then
alive_count=$((alive_count + 1))
else
dead_count=$((dead_count + 1))
fi
# Store session info
sessions_with_health+=("$name|$socket|$target|$status|$pid|$created|$type")
done <<< "$session_names"
fi
#------------------------------------------------------------------------------
# Output results
#------------------------------------------------------------------------------
if [[ "$output_format" == "json" ]]; then
# JSON output
echo "{"
echo " \"sessions\": ["
first=true
for session_info in "${sessions_with_health[@]+"${sessions_with_health[@]}"}"; do
IFS='|' read -r name socket target status pid created type <<< "$session_info"
if [[ "$first" == false ]]; then
echo ","
fi
first=false
# Get basename of socket for cleaner output
socket_basename=$(basename "$socket")
cat << EOF
{
"name": "$name",
"socket": "$socket",
"socket_basename": "$socket_basename",
"target": "$target",
"type": "$type",
"status": "$status",
"pid": ${pid:-null},
"created_at": "$created"
}
EOF
done
echo ""
echo " ],"
echo " \"total\": $total_count,"
echo " \"alive\": $alive_count,"
echo " \"dead\": $dead_count"
echo "}"
else
# Table output
if [[ $total_count -eq 0 ]]; then
echo "No sessions registered."
exit 0
fi
# Print header
printf "%-20s %-20s %-20s %-10s %-8s %-15s\n" \
"NAME" "SOCKET" "TARGET" "STATUS" "PID" "CREATED"
printf "%-20s %-20s %-20s %-10s %-8s %-15s\n" \
"----" "------" "------" "------" "---" "-------"
# Print sessions
for session_info in "${sessions_with_health[@]+"${sessions_with_health[@]}"}"; do
IFS='|' read -r name socket target status pid created type <<< "$session_info"
# Get basename of socket for cleaner output
socket_basename=$(basename "$socket")
# Format time ago
time_str=$(time_ago "$created")
# Truncate long values
name_trunc="${name:0:20}"
socket_trunc="${socket_basename:0:20}"
target_trunc="${target:0:20}"
printf "%-20s %-20s %-20s %-10s %-8s %-15s\n" \
"$name_trunc" "$socket_trunc" "$target_trunc" "$status" "${pid:--}" "$time_str"
done
# Print summary
echo ""
echo "Total: $total_count | Alive: $alive_count | Dead: $dead_count"
fi
exit 0

424
tools/pane-health.sh Executable file
View File

@@ -0,0 +1,424 @@
#!/usr/bin/env bash
#
# pane-health.sh - Check health status of a tmux pane
#
# PURPOSE:
# Verify pane and session state before operations to prevent "pane not found"
# errors and detect failures early. Essential for reliable tmux automation.
#
# HOW IT WORKS:
# 1. Check if tmux server is running on the specified socket
# 2. Verify session exists using tmux has-session
# 3. Check if pane exists and get its state (dead/alive, PID)
# 4. Validate process is running via ps command
# 5. Determine overall health status and return structured output
#
# USE CASES:
# - Before sending commands: verify pane is ready
# - After errors: determine if pane crashed
# - Periodic health checks during long operations
# - Cleanup decision: which panes to kill vs keep
# - Integration with other tools (safe-send.sh, etc.)
#
# EXAMPLES:
# # Check pane health in JSON format
# ./pane-health.sh -S /tmp/my.sock -t session:0.0
#
# # Check pane health in text format
# ./pane-health.sh -t myapp:0.0 --format text
#
# # Use in conditional logic
# if ./pane-health.sh -t session:0.0 --format text; then
# echo "Pane is healthy"
# else
# echo "Pane has issues (exit code: $?)"
# fi
#
# EXIT CODES:
# 0 - Healthy (pane alive, process running)
# 1 - Dead (pane marked as dead)
# 2 - Missing (pane/session doesn't exist)
# 3 - Zombie (process exited but pane still exists)
# 4 - Server not running
#
# DEPENDENCIES:
# - bash (with [[, printf, functions)
# - tmux (for has-session, list-panes)
# - ps (for process state validation)
#
# Bash strict mode:
# -e: Exit immediately if any command fails
# -u: Treat unset variables as errors
# -o pipefail: Pipe fails if any command in pipeline fails
set -euo pipefail
# Get script directory to source registry library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/registry.sh
source "$SCRIPT_DIR/lib/registry.sh"
usage() {
cat <<'USAGE'
Usage: pane-health.sh -t target [options]
OR: pane-health.sh -s session [options]
OR: pane-health.sh [options] # auto-detect single session
Check health status of a tmux pane and report structured results.
Target Selection (priority order):
-s, --session session name (looks up socket/target in registry)
-t, --target tmux target (session:window.pane), explicit
(no flags) auto-detect if only one session in registry
Options:
-S, --socket tmux socket path (for custom sockets via -S)
--format output format: json|text (default: json)
-h, --help show this help
Exit Codes:
0 - Healthy (pane alive, process running)
1 - Dead (pane marked as dead)
2 - Missing (pane/session doesn't exist)
3 - Zombie (process exited but pane still exists)
4 - Server not running
Output Formats:
json - Structured JSON with all health information
text - Human-readable status message
Examples:
# Using session name
./pane-health.sh -s my-python
# Auto-detect single session
./pane-health.sh --format text
# Explicit socket/target (backward compatible)
./pane-health.sh -t session:0.0 -S /tmp/claude.sock
# Use in script with session registry
if ./pane-health.sh -s my-session; then
echo "Pane is ready for commands"
fi
USAGE
}
# ============================================================================
# Default Configuration
# ============================================================================
# Required parameters (must be provided by user)
target="" # tmux target pane (format: session:window.pane)
# Optional parameters
session_name="" # session name for registry lookup
socket="" # tmux socket path (empty = use default tmux socket)
output_format="json" # output format: json or text
# ============================================================================
# Parse Command-Line Arguments
# ============================================================================
while [[ $# -gt 0 ]]; do
case "$1" in
-s|--session) session_name="${2-}"; shift 2 ;; # Set session name for registry lookup
-t|--target) target="${2-}"; shift 2 ;; # Set target pane
-S|--socket) socket="${2-}"; shift 2 ;; # Set custom socket path
--format) output_format="${2-}"; shift 2 ;; # Set output format
-h|--help) usage; exit 0 ;; # Show help and exit
*) echo "Unknown option: $1" >&2; usage; exit 1 ;; # Error on unknown option
esac
done
# ============================================================================
# Session Resolution
# ============================================================================
# Resolve session name to socket/target if provided
# Priority: 1) Explicit -S/-t, 2) Session name -s, 3) Auto-detect single session
if [[ -n "$socket" && -n "$target" ]]; then
# Priority 1: Explicit socket and target provided (backward compatible)
: # Use as-is, no resolution needed
elif [[ -n "$session_name" ]]; then
# Priority 2: Session name provided, look up in registry
if ! session_data=$(registry_get_session "$session_name" 2>/dev/null); then
echo "Error: Session '$session_name' not found in registry" >&2
echo "Use 'list-sessions.sh' to see available sessions" >&2
exit 1
fi
# Extract socket and target from session data
socket=$(echo "$session_data" | jq -r '.socket')
target=$(echo "$session_data" | jq -r '.target')
# Update activity timestamp
registry_update_activity "$session_name" 2>/dev/null || true
elif [[ -z "$socket" && -z "$target" ]]; then
# Priority 3: No explicit params, try auto-detect single session
session_count=$(registry_list_sessions 2>/dev/null | jq '.sessions | length' 2>/dev/null || echo "0")
if [[ "$session_count" == "1" ]]; then
# Single session exists, auto-use it
auto_session_name=$(registry_list_sessions | jq -r '.sessions | keys[0]')
session_data=$(registry_get_session "$auto_session_name")
socket=$(echo "$session_data" | jq -r '.socket')
target=$(echo "$session_data" | jq -r '.target')
# Update activity timestamp
registry_update_activity "$auto_session_name" 2>/dev/null || true
elif [[ "$session_count" == "0" ]]; then
echo "Error: No sessions found in registry" >&2
echo "Create a session with 'create-session.sh' or specify -t and -S explicitly" >&2
exit 1
else
echo "Error: Multiple sessions found ($session_count total)" >&2
echo "Please specify session name with -s or use -t/-S explicitly" >&2
echo "Use 'list-sessions.sh' to see available sessions" >&2
exit 1
fi
fi
# ============================================================================
# Validate Required Parameters and Dependencies
# ============================================================================
# Check that required parameters were provided (after resolution)
if [[ -z "$target" ]]; then
echo "target is required" >&2
usage
exit 1
fi
# Validate output format
if [[ "$output_format" != "json" && "$output_format" != "text" ]]; then
echo "format must be 'json' or 'text'" >&2
exit 1
fi
# Check that tmux is installed and available in PATH
if ! command -v tmux >/dev/null 2>&1; then
echo "tmux not found in PATH" >&2
exit 1
fi
# Check that ps is installed and available in PATH
if ! command -v ps >/dev/null 2>&1; then
echo "ps not found in PATH" >&2
exit 1
fi
# ============================================================================
# Build tmux Command Array
# ============================================================================
# Build base tmux command with optional socket parameter
# ${socket:+-S "$socket"} expands to "-S $socket" if socket is set, empty otherwise
tmux_cmd=(tmux ${socket:+-S "$socket"})
# ============================================================================
# Initialize Health State Variables
# ============================================================================
# Health check results (will be populated during checks)
server_running=false # Is tmux server running on the socket?
session_exists=false # Does the target session exist?
pane_exists=false # Does the target pane exist?
pane_dead=false # Is the pane marked as dead by tmux?
pid="" # Process ID running in the pane
process_running=false # Is the process actually running?
status="unknown" # Overall health status
exit_code=0 # Script exit code
# ============================================================================
# Function: output_result
# ============================================================================
# Output health check results in requested format (JSON or text)
#
# Arguments: None (uses global variables)
# Returns: None (outputs to stdout)
#
output_result() {
if [[ "$output_format" == "json" ]]; then
# --------------------------------------------------------------------------
# JSON Output Format
# --------------------------------------------------------------------------
# Structured output with all health information
# Boolean values: "true" or "false" (JSON strings)
# PID: numeric value or null if not available
#
cat <<JSON
{
"status": "$status",
"server_running": $([[ "$server_running" == true ]] && echo "true" || echo "false"),
"session_exists": $([[ "$session_exists" == true ]] && echo "true" || echo "false"),
"pane_exists": $([[ "$pane_exists" == true ]] && echo "true" || echo "false"),
"pane_dead": $([[ "$pane_dead" == true ]] && echo "true" || echo "false"),
"pid": $([[ -n "$pid" ]] && echo "$pid" || echo "null"),
"process_running": $([[ "$process_running" == true ]] && echo "true" || echo "false")
}
JSON
else
# --------------------------------------------------------------------------
# Text Output Format
# --------------------------------------------------------------------------
# Human-readable status message
#
case "$status" in
healthy)
echo "Pane $target is healthy (PID: $pid, process running)"
;;
dead)
echo "Pane $target is dead (marked as dead by tmux)"
;;
zombie)
echo "Pane $target is a zombie (pane exists but process $pid exited)"
;;
missing)
if [[ "$session_exists" == false ]]; then
echo "Session does not exist (target: $target)"
else
echo "Pane does not exist (target: $target)"
fi
;;
server_not_running)
echo "tmux server is not running on socket${socket:+: $socket}"
;;
*)
echo "Unknown status: $status"
;;
esac
fi
}
# ============================================================================
# Health Check: Step 1 - Check if tmux server is running
# ============================================================================
# Try to list sessions to verify server is running
# Redirect all output to /dev/null (we only care about exit code)
# Exit code 0 = server running, non-zero = server not running
if "${tmux_cmd[@]}" list-sessions >/dev/null 2>&1; then
server_running=true
else
# Server is not running - this is the most fundamental failure
status="server_not_running"
exit_code=4
output_result
exit "$exit_code"
fi
# ============================================================================
# Health Check: Step 2 - Extract session name from target
# ============================================================================
# Target format: session:window.pane or session:window or just session
# Extract session name (everything before first colon, or entire string if no colon)
# ${target%%:*} means: remove longest match of ":*" from the end
session_name="${target%%:*}"
# ============================================================================
# Health Check: Step 3 - Check if session exists
# ============================================================================
# Use tmux has-session to check if session exists
# -t: target session name
# Exit code 0 = session exists, non-zero = session doesn't exist
if "${tmux_cmd[@]}" has-session -t "$session_name" 2>/dev/null; then
session_exists=true
else
# Session doesn't exist - can't check pane without session
status="missing"
exit_code=2
output_result
exit "$exit_code"
fi
# ============================================================================
# Health Check: Step 4 - Check if pane exists and get pane state
# ============================================================================
# Query tmux for pane information
# list-panes -F: Format output with specific variables
# #{pane_dead}: 1 if pane is dead, 0 if alive
# #{pane_pid}: Process ID running in the pane
# -t: target (can be session, session:window, or session:window.pane)
# 2>/dev/null: Suppress errors if pane doesn't exist
#
# Note: If target is session:0.0 but pane doesn't exist, list-panes returns empty
# If target is just session, it lists all panes in session
#
if ! pane_info="$("${tmux_cmd[@]}" list-panes -F '#{pane_dead} #{pane_pid}' -t "$target" 2>/dev/null)"; then
# list-panes failed - pane doesn't exist
status="missing"
exit_code=2
output_result
exit "$exit_code"
fi
# Check if we got any output (pane exists)
if [[ -z "$pane_info" ]]; then
# No output means pane doesn't exist
status="missing"
exit_code=2
output_result
exit "$exit_code"
fi
# Pane exists - mark it and parse the info
pane_exists=true
# --------------------------------------------------------------------------
# Parse pane information
# --------------------------------------------------------------------------
# pane_info format: "0 12345" or "1 12345"
# First field: pane_dead flag (0 = alive, 1 = dead)
# Second field: pane_pid
#
# Read into variables using read command
# IFS=' ': Use space as field separator
# read -r: Don't interpret backslashes
read -r pane_dead_flag pid_value <<< "$pane_info"
# Set pane_dead boolean based on flag
if [[ "$pane_dead_flag" == "1" ]]; then
pane_dead=true
status="dead"
exit_code=1
output_result
exit "$exit_code"
fi
# Store PID value
pid="$pid_value"
# ============================================================================
# Health Check: Step 5 - Validate process is running
# ============================================================================
# Use ps to check if process with this PID is actually running
# -p: Specify process ID to check
# -o pid=: Output only PID (with no header)
# 2>/dev/null: Suppress errors if PID doesn't exist
# grep -q: Quiet mode, just check if pattern matches (exit code 0 = match)
#
if ps -p "$pid" -o pid= >/dev/null 2>&1; then
# Process is running - pane is healthy!
process_running=true
status="healthy"
exit_code=0
else
# Process is not running but pane still exists - this is a zombie
# This can happen when a process exits but tmux keeps the pane open
# (depending on remain-on-exit setting)
process_running=false
status="zombie"
exit_code=3
fi
# ============================================================================
# Output Results and Exit
# ============================================================================
output_result
exit "$exit_code"

503
tools/safe-send.sh Executable file
View File

@@ -0,0 +1,503 @@
#!/usr/bin/env bash
#
# safe-send.sh - Send keystrokes to tmux pane with retries and readiness checking
#
# PURPOSE:
# Reliably send commands to tmux panes with automatic retries, readiness checks,
# and optional prompt waiting. Prevents dropped keystrokes that can occur when
# sending to busy or not-yet-ready panes.
#
# HOW IT WORKS:
# 1. Verify pane is healthy using pane-health.sh (if available)
# 2. Attempt to send keystrokes using tmux send-keys
# 3. On failure: retry with exponential backoff (0.5s, 1s, 2s, ...)
# 4. Optionally wait for prompt pattern after sending (using wait-for-text.sh)
# 5. Return success or failure with appropriate exit code
#
# USE CASES:
# - Send commands to Python REPL with automatic retry
# - Send gdb commands and wait for prompt
# - Critical commands that must not be dropped
# - Send commands immediately after session creation
# - Integrate into automation scripts requiring reliability
#
# EXAMPLES:
# # Send Python command and wait for prompt
# ./safe-send.sh -S /tmp/my.sock -t session:0.0 -c "print('hello')" -w ">>>"
#
# # Send literal text without executing (no Enter)
# ./safe-send.sh -t myapp:0.0 -c "some text" -l
#
# # Send with custom retry settings
# ./safe-send.sh -t session:0.0 -c "ls" -r 5 -i 1.0 -T 60
#
# # Send control sequence
# ./safe-send.sh -t session:0.0 -c "C-c"
#
# EXIT CODES:
# 0 - Command sent successfully
# 1 - Failed to send after retries
# 2 - Timeout waiting for prompt
# 3 - Pane not ready
# 4 - Invalid arguments
#
# DEPENDENCIES:
# - bash (with [[, printf, sleep, bc for exponential backoff)
# - tmux (for send-keys)
# - pane-health.sh (optional, for readiness check)
# - wait-for-text.sh (optional, for prompt waiting)
#
# Bash strict mode:
# -e: Exit immediately if any command fails
# -u: Treat unset variables as errors
# -o pipefail: Pipe fails if any command in pipeline fails
set -euo pipefail
# Get script directory to source registry library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/registry.sh
source "$SCRIPT_DIR/lib/registry.sh"
usage() {
cat <<'USAGE'
Usage: safe-send.sh -t target -c command [options]
OR: safe-send.sh -s session -c command [options]
OR: safe-send.sh -c command [options] # auto-detect single session
Send keystrokes to a tmux pane with automatic retries and readiness checking.
Target Selection (priority order):
-s, --session session name (looks up socket/target in registry)
-t, --target tmux target (session:window.pane), explicit
(no flags) auto-detect if only one session in registry
Options:
-c, --command command to send (empty = just Enter)
-S, --socket tmux socket path (for custom sockets via -S)
-L, --socket-name tmux socket name (for named sockets via -L)
-l, --literal use literal mode (send-keys -l, no Enter)
-m, --multiline use multiline mode (paste-buffer for code blocks)
-w, --wait wait for this pattern after sending
-T, --timeout timeout in seconds (default: 30)
-r, --retries max retry attempts (default: 3)
-i, --interval base retry interval in seconds (default: 0.5)
-v, --verbose verbose output for debugging
-h, --help show this help
Exit Codes:
0 - Command sent successfully
1 - Failed to send after retries
2 - Timeout waiting for prompt
3 - Pane not ready
4 - Invalid arguments
Modes:
Normal mode (default):
Sends command and presses Enter (executes in shell/REPL)
Example: safe-send.sh -c "print('hello')"
Multiline mode (-m):
Sends multiline code blocks via paste-buffer
Auto-appends blank line for REPL execution
Example: safe-send.sh -m -c "def foo():
return 42"
(Incompatible with --literal)
Literal mode (-l):
Sends exact characters without Enter (typing text)
Example: safe-send.sh -l -c "some text"
(Incompatible with --multiline)
Examples:
# Send Python command and wait for prompt
safe-send.sh -t session:0.0 -c "2+2" -w ">>>" -T 10
# Send multiline Python function
safe-send.sh -t session:0.0 -m -c "def foo():
return 42" -w ">>>"
# Send gdb command
safe-send.sh -t debug:0.0 -c "break main" -w "(gdb)"
# Send with literal mode (no Enter)
safe-send.sh -t session:0.0 -c "text" -l
# Send with custom retry settings
safe-send.sh -t session:0.0 -c "ls" -r 5 -i 1.0
# Send on named socket
safe-send.sh -L my-socket -t session:0.0 -c "echo test"
USAGE
}
# ============================================================================
# Default Configuration
# ============================================================================
# Required parameters (must be provided by user)
target="" # tmux target pane (format: session:window.pane)
command="__NOT_SET__" # command to send to the pane (sentinel value = not provided)
# Optional parameters
session_name="" # session name for registry lookup
socket="" # tmux socket path (empty = use default tmux socket)
socket_name="" # tmux socket name (for -L option)
literal_mode=false # use send-keys -l (literal mode)
multiline_mode=false # use paste-buffer for multiline code blocks
wait_pattern="" # pattern to wait for after sending (optional)
timeout=30 # timeout in seconds for prompt waiting
max_retries=3 # maximum number of send attempts
base_interval=0.5 # base interval for exponential backoff
verbose=false # enable verbose logging
# ============================================================================
# Parse Command-Line Arguments
# ============================================================================
while [[ $# -gt 0 ]]; do
case "$1" in
-s|--session) session_name="${2-}"; shift 2 ;;
-t|--target) target="${2-}"; shift 2 ;;
-c|--command) command="${2-}"; shift 2 ;;
-S|--socket) socket="${2-}"; shift 2 ;;
-L|--socket-name) socket_name="${2-}"; shift 2 ;;
-l|--literal) literal_mode=true; shift ;;
-m|--multiline) multiline_mode=true; shift ;;
-w|--wait) wait_pattern="${2-}"; shift 2 ;;
-T|--timeout) timeout="${2-}"; shift 2 ;;
-r|--retries) max_retries="${2-}"; shift 2 ;;
-i|--interval) base_interval="${2-}"; shift 2 ;;
-v|--verbose) verbose=true; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage; exit 4 ;;
esac
done
# ============================================================================
# Session Resolution
# ============================================================================
# Resolve session name to socket/target if provided
# Priority: 1) Explicit -S/-t, 2) Session name -s, 3) Auto-detect single session
if [[ -n "$socket" && -n "$target" ]]; then
# Priority 1: Explicit socket and target provided (backward compatible)
: # Use as-is, no resolution needed
elif [[ -n "$session_name" ]]; then
# Priority 2: Session name provided, look up in registry
if ! session_data=$(registry_get_session "$session_name" 2>/dev/null); then
echo "Error: Session '$session_name' not found in registry" >&2
echo "Use 'list-sessions.sh' to see available sessions" >&2
exit 4
fi
# Extract socket and target from session data
socket=$(echo "$session_data" | jq -r '.socket')
target=$(echo "$session_data" | jq -r '.target')
# Update activity timestamp
registry_update_activity "$session_name" 2>/dev/null || true
if [[ "$verbose" == true ]]; then
echo "Resolved session '$session_name': socket=$socket, target=$target" >&2
fi
elif [[ -z "$socket" && -z "$target" ]]; then
# Priority 3: No explicit params, try auto-detect single session
session_count=$(registry_list_sessions 2>/dev/null | jq '.sessions | length' 2>/dev/null || echo "0")
if [[ "$session_count" == "1" ]]; then
# Single session exists, auto-use it
auto_session_name=$(registry_list_sessions | jq -r '.sessions | keys[0]')
session_data=$(registry_get_session "$auto_session_name")
socket=$(echo "$session_data" | jq -r '.socket')
target=$(echo "$session_data" | jq -r '.target')
# Update activity timestamp
registry_update_activity "$auto_session_name" 2>/dev/null || true
if [[ "$verbose" == true ]]; then
echo "Auto-detected single session '$auto_session_name': socket=$socket, target=$target" >&2
fi
elif [[ "$session_count" == "0" ]]; then
echo "Error: No sessions found in registry" >&2
echo "Create a session with 'create-session.sh' or specify -t and -S explicitly" >&2
exit 4
else
echo "Error: Multiple sessions found ($session_count total)" >&2
echo "Please specify session name with -s or use -t/-S explicitly" >&2
echo "Use 'list-sessions.sh' to see available sessions" >&2
exit 4
fi
fi
# ============================================================================
# Validate Required Parameters and Dependencies
# ============================================================================
# Check that required parameters were provided (after resolution)
if [[ -z "$target" ]]; then
echo "target is required" >&2
usage
exit 4
fi
# Check that -c was provided (but empty string is allowed)
if [[ "$command" == "__NOT_SET__" ]]; then
echo "command is required (use -c \"\" to send just Enter)" >&2
usage
exit 4
fi
# Note: Empty command is allowed - it just sends Enter (useful for prompts)
# Validate that timeout is a positive number
if ! [[ "$timeout" =~ ^[0-9]+\.?[0-9]*$ ]]; then
echo "timeout must be a positive number" >&2
exit 4
fi
# Validate that max_retries is a positive integer
if ! [[ "$max_retries" =~ ^[0-9]+$ ]] || [[ "$max_retries" -lt 1 ]]; then
echo "retries must be a positive integer (>= 1)" >&2
exit 4
fi
# Validate that base_interval is a positive number
if ! [[ "$base_interval" =~ ^[0-9]+\.?[0-9]*$ ]]; then
echo "interval must be a positive number" >&2
exit 4
fi
# Check that both socket options are not specified
if [[ -n "$socket" && -n "$socket_name" ]]; then
echo "Cannot specify both -S and -L options" >&2
exit 4
fi
# Check that multiline and literal modes are not both specified
if [[ "$multiline_mode" == true && "$literal_mode" == true ]]; then
echo "Error: --multiline and --literal are mutually exclusive" >&2
exit 4
fi
# Check that tmux is installed and available in PATH
if ! command -v tmux >/dev/null 2>&1; then
echo "tmux not found in PATH" >&2
exit 4
fi
# ============================================================================
# Helper Functions
# ============================================================================
# verbose_log: Log message if verbose mode is enabled
# Arguments: message string
# Returns: None (outputs to stderr)
verbose_log() {
if [[ "$verbose" == true ]]; then
echo "[safe-send] $*" >&2
fi
}
# ============================================================================
# Build tmux Command Array
# ============================================================================
# Build base tmux command with optional socket parameter
tmux_cmd=(tmux)
if [[ -n "$socket" ]]; then
tmux_cmd+=(-S "$socket")
verbose_log "Using socket path: $socket"
elif [[ -n "$socket_name" ]]; then
tmux_cmd+=(-L "$socket_name")
verbose_log "Using socket name: $socket_name"
fi
# ============================================================================
# Get Tool Directory for Optional Dependencies
# ============================================================================
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ============================================================================
# Pre-flight Check: Verify Pane Health
# ============================================================================
# Check if pane-health.sh is available
pane_health_tool="$SCRIPT_DIR/pane-health.sh"
if [[ -x "$pane_health_tool" ]]; then
verbose_log "Checking pane health before sending..."
# Build socket args for pane-health.sh
pane_health_args=()
if [[ -n "$socket" ]]; then
pane_health_args+=(-S "$socket")
elif [[ -n "$socket_name" ]]; then
# pane-health.sh doesn't support -L, so use default socket when -L is used
# This means health check won't work perfectly with -L, but we'll skip it gracefully
verbose_log "Warning: pane-health.sh doesn't support -L option, skipping health check"
fi
# Check pane health (exit codes: 0=healthy, 1=dead, 2=missing, 3=zombie, 4=server not running)
# Only run health check if we have socket args (not using -L)
if [[ ${#pane_health_args[@]} -gt 0 ]]; then
if ! "$pane_health_tool" "${pane_health_args[@]}" -t "$target" --format text >/dev/null 2>&1; then
health_exit=$?
echo "Error: Pane not ready (health check failed with exit code $health_exit)" >&2
verbose_log "Run '$pane_health_tool -t $target --format text' for details"
exit 3
fi
verbose_log "Pane health check passed"
fi
else
verbose_log "pane-health.sh not found, skipping health check"
fi
# ============================================================================
# Main Logic: Send Command with Retry
# ============================================================================
send_success=false
for attempt in $(seq 1 "$max_retries"); do
verbose_log "Attempt $attempt/$max_retries: Sending command to $target"
if [[ "$multiline_mode" == true ]]; then
# ============================================================
# Multiline mode: use paste-buffer
# ============================================================
verbose_log "Using multiline mode (paste-buffer)"
# Auto-append blank line if not present (for Python REPL execution)
processed_command="$command"
if [[ ! "$processed_command" =~ $'\n\n'$ ]]; then
processed_command="${processed_command}"$'\n\n'
verbose_log "Auto-appended blank line for REPL execution"
fi
# Set buffer
if ! "${tmux_cmd[@]}" set-buffer "$processed_command" 2>/dev/null; then
verbose_log "set-buffer failed on attempt $attempt"
# Continue to retry logic below (don't break early)
else
# Paste buffer to target pane
if "${tmux_cmd[@]}" paste-buffer -t "$target" 2>/dev/null; then
verbose_log "paste-buffer successful on attempt $attempt"
send_success=true
break
else
verbose_log "paste-buffer failed on attempt $attempt"
# Continue to retry logic below
fi
fi
elif [[ "$literal_mode" == true ]]; then
# ============================================================
# Literal mode: send exact characters, no Enter
# ============================================================
verbose_log "Using literal mode (-l)"
send_cmd=("${tmux_cmd[@]}" send-keys -t "$target")
send_cmd+=(-l "$command")
# Attempt to send the command
if "${send_cmd[@]}" 2>/dev/null; then
verbose_log "Send successful on attempt $attempt"
send_success=true
break
else
verbose_log "Send failed on attempt $attempt"
fi
else
# ============================================================
# Normal mode: send command and press Enter
# ============================================================
verbose_log "Using normal mode (with Enter)"
send_cmd=("${tmux_cmd[@]}" send-keys -t "$target")
send_cmd+=("$command" Enter)
# Attempt to send the command
if "${send_cmd[@]}" 2>/dev/null; then
verbose_log "Send successful on attempt $attempt"
send_success=true
break
else
verbose_log "Send failed on attempt $attempt"
fi
fi
# ============================================================
# Retry logic with exponential backoff
# ============================================================
# If this is not the last attempt, wait before retrying
if [[ $attempt -lt $max_retries ]]; then
# Calculate exponential backoff: base_interval * (2 ^ (attempt - 1))
# For base_interval=0.5: 0.5s, 1s, 2s, 4s, ...
# Using bc for floating-point arithmetic
if command -v bc >/dev/null 2>&1; then
sleep_duration=$(echo "$base_interval * (2 ^ ($attempt - 1))" | bc -l)
else
# Fallback if bc is not available: use integer arithmetic
multiplier=$((2 ** (attempt - 1)))
sleep_duration=$(echo "$base_interval * $multiplier" | awk '{print $1 * $3}')
fi
verbose_log "Waiting ${sleep_duration}s before retry..."
sleep "$sleep_duration"
fi
done
# Check if send was successful
if [[ "$send_success" == false ]]; then
echo "Error: Failed to send command after $max_retries attempts" >&2
exit 1
fi
# ============================================================================
# Optional: Wait for Prompt Pattern
# ============================================================================
# If wait pattern is specified, wait for it using wait-for-text.sh
if [[ -n "$wait_pattern" ]]; then
wait_tool="$SCRIPT_DIR/wait-for-text.sh"
if [[ -x "$wait_tool" ]]; then
verbose_log "Waiting for pattern: $wait_pattern (timeout: ${timeout}s)"
# Build socket args for wait-for-text.sh
wait_args=()
if [[ -n "$socket" ]]; then
wait_args+=(-S "$socket")
elif [[ -n "$socket_name" ]]; then
# wait-for-text.sh doesn't support -L, skip waiting with warning
echo "Warning: wait-for-text.sh doesn't support -L option, cannot wait for pattern" >&2
verbose_log "Skipping pattern wait due to -L usage"
# Exit successfully since the send was successful
exit 0
fi
# Wait for pattern (only if using -S or default socket)
if [[ ${#wait_args[@]} -ge 0 ]]; then
if "$wait_tool" "${wait_args[@]}" -t "$target" -p "$wait_pattern" -T "$timeout" >/dev/null 2>&1; then
verbose_log "Pattern found"
exit 0
else
wait_exit=$?
echo "Error: Timeout waiting for pattern '$wait_pattern'" >&2
verbose_log "wait-for-text.sh exited with code $wait_exit"
exit 2
fi
fi
else
echo "Warning: wait-for-text.sh not found, cannot wait for pattern" >&2
verbose_log "Continuing without waiting for pattern"
fi
fi
# ============================================================================
# Success
# ============================================================================
verbose_log "Command sent successfully"
exit 0

260
tools/wait-for-text.sh Executable file
View File

@@ -0,0 +1,260 @@
#!/usr/bin/env bash
#
# wait-for-text.sh - Poll a tmux pane for text pattern and exit when found
#
# PURPOSE:
# Synchronize with interactive programs running in tmux by waiting for specific
# output patterns (e.g., shell prompts, program completion messages).
#
# HOW IT WORKS:
# 1. Captures pane output at regular intervals (default: 0.5s)
# 2. Searches captured text for the specified pattern using grep
# 3. Exits successfully (0) when pattern is found
# 4. Exits with error (1) if timeout is reached
#
# USE CASES:
# - Wait for Python REPL prompt (>>>) before sending commands
# - Wait for gdb prompt before issuing breakpoint commands
# - Wait for "compilation complete" before running tests
# - Synchronize with any interactive CLI tool in tmux
#
# EXAMPLE:
# # Wait for Python prompt on custom socket
# ./wait-for-text.sh -S /tmp/my.sock -t session:0.0 -p '^>>>' -T 10
#
# # Wait for exact string "Ready" (fixed string, not regex)
# ./wait-for-text.sh -t myapp:0.0 -p 'Ready' -F -T 30
#
# DEPENDENCIES:
# - bash (with [[, printf, sleep)
# - tmux (for capture-pane)
# - grep (for pattern matching)
# - date (for timeout calculation)
#
# Bash strict mode:
# -e: Exit immediately if any command fails
# -u: Treat unset variables as errors
# -o pipefail: Pipe fails if any command in pipeline fails
set -euo pipefail
# Get script directory to source registry library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/registry.sh
source "$SCRIPT_DIR/lib/registry.sh"
usage() {
cat <<'USAGE'
Usage: wait-for-text.sh -t target -p pattern [options]
OR: wait-for-text.sh -s session -p pattern [options]
OR: wait-for-text.sh -p pattern [options] # auto-detect single session
Poll a tmux pane for text and exit when found.
Target Selection (priority order):
-s, --session session name (looks up socket/target in registry)
-t, --target tmux target (session:window.pane), explicit
(no flags) auto-detect if only one session in registry
Options:
-p, --pattern regex pattern to look for, required
-S, --socket tmux socket path (for custom sockets via -S)
-F, --fixed treat pattern as a fixed string (grep -F)
-T, --timeout seconds to wait (integer, default: 15)
-i, --interval poll interval in seconds (default: 0.5)
-l, --lines number of history lines to inspect (integer, default: 1000)
-h, --help show this help
Examples:
# Using session name
wait-for-text.sh -s my-python -p '>>>' -T 10
# Auto-detect single session
wait-for-text.sh -p '>>>' -T 10
# Explicit socket/target (backward compatible)
wait-for-text.sh -S /tmp/my.sock -t session:0.0 -p '>>>'
USAGE
}
# ============================================================================
# Default Configuration
# ============================================================================
# Required parameters (must be provided by user)
target="" # tmux target pane (format: session:window.pane)
pattern="" # regex pattern or fixed string to search for
# Optional parameters
session_name="" # session name for registry lookup
socket="" # tmux socket path (empty = use default tmux socket)
grep_flag="-E" # grep mode: -E (extended regex, default) or -F (fixed string)
timeout=15 # seconds to wait before giving up
interval=0.5 # seconds between polling attempts
lines=1000 # number of pane history lines to capture and search
# ============================================================================
# Parse Command-Line Arguments
# ============================================================================
while [[ $# -gt 0 ]]; do
case "$1" in
-s|--session) session_name="${2-}"; shift 2 ;; # Set session name for registry lookup
-t|--target) target="${2-}"; shift 2 ;; # Set target pane
-p|--pattern) pattern="${2-}"; shift 2 ;; # Set search pattern
-S|--socket) socket="${2-}"; shift 2 ;; # Set custom socket path
-F|--fixed) grep_flag="-F"; shift ;; # Use fixed string matching
-T|--timeout) timeout="${2-}"; shift 2 ;; # Set timeout duration
-i|--interval) interval="${2-}"; shift 2 ;; # Set poll interval
-l|--lines) lines="${2-}"; shift 2 ;; # Set history depth
-h|--help) usage; exit 0 ;; # Show help and exit
*) echo "Unknown option: $1" >&2; usage; exit 1 ;; # Error on unknown option
esac
done
# ============================================================================
# Session Resolution
# ============================================================================
# Resolve session name to socket/target if provided
# Priority: 1) Explicit -S/-t, 2) Session name -s, 3) Auto-detect single session
if [[ -n "$socket" && -n "$target" ]]; then
# Priority 1: Explicit socket and target provided (backward compatible)
: # Use as-is, no resolution needed
elif [[ -n "$session_name" ]]; then
# Priority 2: Session name provided, look up in registry
if ! session_data=$(registry_get_session "$session_name" 2>/dev/null); then
echo "Error: Session '$session_name' not found in registry" >&2
echo "Use 'list-sessions.sh' to see available sessions" >&2
exit 1
fi
# Extract socket and target from session data
socket=$(echo "$session_data" | jq -r '.socket')
target=$(echo "$session_data" | jq -r '.target')
# Update activity timestamp
registry_update_activity "$session_name" 2>/dev/null || true
elif [[ -z "$socket" && -z "$target" ]]; then
# Priority 3: No explicit params, try auto-detect single session
session_count=$(registry_list_sessions 2>/dev/null | jq '.sessions | length' 2>/dev/null || echo "0")
if [[ "$session_count" == "1" ]]; then
# Single session exists, auto-use it
auto_session_name=$(registry_list_sessions | jq -r '.sessions | keys[0]')
session_data=$(registry_get_session "$auto_session_name")
socket=$(echo "$session_data" | jq -r '.socket')
target=$(echo "$session_data" | jq -r '.target')
# Update activity timestamp
registry_update_activity "$auto_session_name" 2>/dev/null || true
elif [[ "$session_count" == "0" ]]; then
echo "Error: No sessions found in registry" >&2
echo "Create a session with 'create-session.sh' or specify -t and -S explicitly" >&2
exit 1
else
echo "Error: Multiple sessions found ($session_count total)" >&2
echo "Please specify session name with -s or use -t/-S explicitly" >&2
echo "Use 'list-sessions.sh' to see available sessions" >&2
exit 1
fi
fi
# ============================================================================
# Validate Required Parameters and Dependencies
# ============================================================================
# Check that required parameters were provided (after resolution)
if [[ -z "$target" || -z "$pattern" ]]; then
echo "target and pattern are required" >&2
usage
exit 1
fi
# Validate that timeout is a positive integer (regex: one or more digits)
if ! [[ "$timeout" =~ ^[0-9]+$ ]]; then
echo "timeout must be an integer number of seconds" >&2
exit 1
fi
# Validate that lines is a positive integer
if ! [[ "$lines" =~ ^[0-9]+$ ]]; then
echo "lines must be an integer" >&2
exit 1
fi
# Check that tmux is installed and available in PATH
if ! command -v tmux >/dev/null 2>&1; then
echo "tmux not found in PATH" >&2
exit 1
fi
# ============================================================================
# Calculate Deadline for Timeout
# ============================================================================
# Get current time in epoch seconds (Unix timestamp)
start_epoch=$(date +%s)
# Calculate deadline: current time + timeout duration
deadline=$((start_epoch + timeout))
# ============================================================================
# Main Polling Loop
# ============================================================================
# Repeatedly capture pane output and search for pattern until found or timeout
while true; do
# --------------------------------------------------------------------------
# Step 1: Capture pane output from tmux
# --------------------------------------------------------------------------
# tmux capture-pane options:
# -p: Print to stdout (instead of saving to paste buffer)
# -J: Join wrapped lines (prevents false line breaks from terminal width)
# -t: Target pane to capture from
# -S: Start line (negative = relative to end, e.g., -1000 = last 1000 lines)
#
# ${socket:+-S "$socket"} syntax explanation:
# - If $socket is set: expands to -S "$socket"
# - If $socket is empty: expands to nothing
# This allows optional socket parameter without breaking the command
#
# Error handling:
# 2>/dev/null: Suppress error messages if pane doesn't exist
# || true: Don't fail script if capture fails (exit 0 instead)
#
pane_text="$(tmux ${socket:+-S "$socket"} capture-pane -p -J -t "$target" -S "-${lines}" 2>/dev/null || true)"
# --------------------------------------------------------------------------
# Step 2: Search captured text for pattern
# --------------------------------------------------------------------------
# Use printf to safely output text (handles special characters correctly)
# Pipe to grep to search for pattern
# $grep_flag: Either -E (regex) or -F (fixed string), set by --fixed flag
# --: Marks end of options (allows patterns starting with -)
# >/dev/null: Discard grep output (we only care about exit code)
# Exit code 0 = pattern found, 1 = not found
#
if printf '%s\n' "$pane_text" | grep $grep_flag -- "$pattern" >/dev/null 2>&1; then
# SUCCESS: Pattern found in pane output
exit 0
fi
# --------------------------------------------------------------------------
# Step 3: Check if timeout has been reached
# --------------------------------------------------------------------------
now=$(date +%s)
if (( now >= deadline )); then
# TIMEOUT: Pattern not found within specified time
echo "Timed out after ${timeout}s waiting for pattern: $pattern" >&2
echo "Last ${lines} lines from $target:" >&2
printf '%s\n' "$pane_text" >&2 # Show what was captured (for debugging)
exit 1
fi
# --------------------------------------------------------------------------
# Step 4: Wait before next poll attempt
# --------------------------------------------------------------------------
# Sleep for specified interval before checking again
# Default: 0.5 seconds (configurable via --interval)
sleep "$interval"
done