claude-tmux 1.0.9 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/dist/index.js +56 -34
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -54,6 +54,14 @@ Send a follow-up message to a running Claude session.
|
|
|
54
54
|
send("task-name", "do something else")
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
### list
|
|
58
|
+
|
|
59
|
+
List all active Claude tmux sessions. Useful for discovery across conversations.
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
list()
|
|
63
|
+
```
|
|
64
|
+
|
|
57
65
|
### kill
|
|
58
66
|
|
|
59
67
|
Terminate a Claude tmux session and clean up resources.
|
|
@@ -74,5 +82,5 @@ kill(name) → cleanup
|
|
|
74
82
|
|
|
75
83
|
## Tips
|
|
76
84
|
|
|
85
|
+
- Verify output shows task completion before killing. Idle agents are fine to leave running.
|
|
77
86
|
- Attach manually: `tmux attach -t claude-<name>`
|
|
78
|
-
- Always kill sessions when done to avoid orphaned processes
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,13 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { execSync } from "child_process";
|
|
6
6
|
import { writeFileSync } from "fs";
|
|
7
|
+
// Constants
|
|
7
8
|
const SESSION_PREFIX = "claude-";
|
|
9
|
+
const TIMEOUT_MS = 900000; // 15 minutes
|
|
10
|
+
const POLL_INTERVAL_MS = 2000; // 2 seconds
|
|
11
|
+
const INITIAL_DELAY_MS = 5000; // 5 seconds
|
|
12
|
+
const CAPTURE_LINES = 100;
|
|
13
|
+
const IDLE_THRESHOLD = 5; // consecutive idle polls
|
|
8
14
|
function runTmux(args) {
|
|
9
15
|
return execSync(`tmux ${args}`, { encoding: "utf-8", timeout: 10000 }).trim();
|
|
10
16
|
}
|
|
@@ -16,6 +22,15 @@ function runTmuxSafe(args) {
|
|
|
16
22
|
return null;
|
|
17
23
|
}
|
|
18
24
|
}
|
|
25
|
+
function response(text) {
|
|
26
|
+
return { content: [{ type: "text", text }] };
|
|
27
|
+
}
|
|
28
|
+
function errorMessage(e) {
|
|
29
|
+
return e instanceof Error ? e.message : String(e);
|
|
30
|
+
}
|
|
31
|
+
function sessionExists(session) {
|
|
32
|
+
return runTmuxSafe(`has-session -t "${session}"`) !== null;
|
|
33
|
+
}
|
|
19
34
|
function sessionName(name) {
|
|
20
35
|
return `${SESSION_PREFIX}${name.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
21
36
|
}
|
|
@@ -50,17 +65,15 @@ function filterUIChrome(output) {
|
|
|
50
65
|
return filtered.join('\n').trim();
|
|
51
66
|
}
|
|
52
67
|
async function waitForIdle(session) {
|
|
53
|
-
const timeout = 600000; // 10 minutes
|
|
54
|
-
const lines = 100; // Capture more lines to catch done signal
|
|
55
68
|
const startTime = Date.now();
|
|
56
69
|
let idleCount = 0;
|
|
57
70
|
let output = "";
|
|
58
71
|
// Initial delay to let Claude start working
|
|
59
|
-
await sleep(
|
|
60
|
-
while (Date.now() - startTime <
|
|
61
|
-
await sleep(
|
|
72
|
+
await sleep(INITIAL_DELAY_MS);
|
|
73
|
+
while (Date.now() - startTime < TIMEOUT_MS) {
|
|
74
|
+
await sleep(POLL_INTERVAL_MS);
|
|
62
75
|
try {
|
|
63
|
-
output = runTmux(`capture-pane -t "${session}" -p -S -${
|
|
76
|
+
output = runTmux(`capture-pane -t "${session}" -p -S -${CAPTURE_LINES}`);
|
|
64
77
|
// If busy, reset idle count and continue polling
|
|
65
78
|
if (isBusy(output)) {
|
|
66
79
|
idleCount = 0;
|
|
@@ -71,21 +84,20 @@ async function waitForIdle(session) {
|
|
|
71
84
|
return filterUIChrome(output);
|
|
72
85
|
}
|
|
73
86
|
// Not busy - count consecutive idle polls
|
|
74
|
-
// After 2 consecutive non-busy polls, consider done
|
|
75
87
|
idleCount++;
|
|
76
|
-
if (idleCount >=
|
|
88
|
+
if (idleCount >= IDLE_THRESHOLD) {
|
|
77
89
|
return filterUIChrome(output);
|
|
78
90
|
}
|
|
79
91
|
}
|
|
80
92
|
catch (e) {
|
|
81
|
-
return `Error: ${e
|
|
93
|
+
return `Error: ${errorMessage(e)}`;
|
|
82
94
|
}
|
|
83
95
|
}
|
|
84
|
-
return `Timeout after
|
|
96
|
+
return `Timeout after 15 minutes. Session still running.\n\n${filterUIChrome(output)}`;
|
|
85
97
|
}
|
|
86
98
|
const server = new McpServer({
|
|
87
99
|
name: "claude-tmux",
|
|
88
|
-
version: "1.0
|
|
100
|
+
version: "1.1.0",
|
|
89
101
|
}, {
|
|
90
102
|
instructions: `# claude-tmux: Autonomous Claude Agents
|
|
91
103
|
|
|
@@ -93,23 +105,14 @@ Spawn Claude Code instances in tmux sessions for long-running, independent tasks
|
|
|
93
105
|
|
|
94
106
|
## Tools
|
|
95
107
|
- **spawn**: Start a new Claude session with a prompt.
|
|
96
|
-
- **read**: Wait for a session to finish and return
|
|
97
|
-
- **send**: Send a follow-up message
|
|
98
|
-
- **
|
|
99
|
-
|
|
100
|
-
## Pattern
|
|
101
|
-
|
|
102
|
-
\`\`\`
|
|
103
|
-
spawn(name, prompt, workdir) → start session
|
|
104
|
-
read(name) → wait for completion, get output
|
|
105
|
-
send(name, text) → steer with follow-up
|
|
106
|
-
read(name) → wait for completion, get output
|
|
107
|
-
kill(name) → cleanup
|
|
108
|
-
\`\`\`
|
|
108
|
+
- **read**: Wait for a session to finish and return output.
|
|
109
|
+
- **send**: Send a follow-up message mid-task. Call read after.
|
|
110
|
+
- **list**: List active sessions (useful for discovery across conversations).
|
|
111
|
+
- **kill**: Terminate a session.
|
|
109
112
|
|
|
110
113
|
## Tips
|
|
111
|
-
-
|
|
112
|
-
-
|
|
114
|
+
- Verify output shows task completion before killing. Idle agents are fine to leave running.
|
|
115
|
+
- User can attach manually: \`tmux attach -t claude-<name>\``,
|
|
113
116
|
});
|
|
114
117
|
server.tool("spawn", "Launch a new Claude Code instance in a tmux session. Creates an interactive session you can communicate with via send/read. The session runs until killed. Use for multi-turn conversations or tasks requiring steering.", {
|
|
115
118
|
name: z.string().min(1).max(50).describe("Unique session name (e.g., 'refactor-auth', 'debug-api')"),
|
|
@@ -122,25 +125,31 @@ server.tool("spawn", "Launch a new Claude Code instance in a tmux session. Creat
|
|
|
122
125
|
runTmux(`new-session -d -s "${session}" -c "${workdir}"`);
|
|
123
126
|
}
|
|
124
127
|
catch (e) {
|
|
125
|
-
return
|
|
128
|
+
return response(`Error: ${errorMessage(e)}`);
|
|
126
129
|
}
|
|
127
130
|
const tempFile = `/tmp/claude-prompt-${session}.txt`;
|
|
128
131
|
writeFileSync(tempFile, prompt);
|
|
129
132
|
runTmux(`send-keys -t "${session}" 'claude --dangerously-skip-permissions "$(cat ${tempFile})" && rm ${tempFile}' Enter`);
|
|
130
|
-
return
|
|
133
|
+
return response(`Started ${session}`);
|
|
131
134
|
});
|
|
132
135
|
server.tool("read", "Wait for a Claude session to finish working and return the terminal output. You can continue other work while waiting.", {
|
|
133
136
|
name: z.string().describe("Session name (as provided to spawn)"),
|
|
134
137
|
}, async ({ name }) => {
|
|
135
138
|
const session = sessionName(name);
|
|
139
|
+
if (!sessionExists(session)) {
|
|
140
|
+
return response(`Session '${name}' does not exist`);
|
|
141
|
+
}
|
|
136
142
|
const output = await waitForIdle(session);
|
|
137
|
-
return
|
|
143
|
+
return response(output);
|
|
138
144
|
});
|
|
139
145
|
server.tool("send", "Send a follow-up message to a running Claude session. Use to steer the session mid-task or provide additional instructions. Call read afterwards to get the result.", {
|
|
140
146
|
name: z.string().describe("Session name (as provided to spawn)"),
|
|
141
147
|
text: z.string().describe("Message to send to Claude"),
|
|
142
148
|
}, async ({ name, text }) => {
|
|
143
149
|
const session = sessionName(name);
|
|
150
|
+
if (!sessionExists(session)) {
|
|
151
|
+
return response(`Session '${name}' does not exist`);
|
|
152
|
+
}
|
|
144
153
|
try {
|
|
145
154
|
const escaped = text
|
|
146
155
|
.replace(/\\/g, '\\\\')
|
|
@@ -149,23 +158,36 @@ server.tool("send", "Send a follow-up message to a running Claude session. Use t
|
|
|
149
158
|
.replace(/`/g, '\\`');
|
|
150
159
|
runTmux(`send-keys -t "${session}" -l "${escaped}"`);
|
|
151
160
|
runTmux(`send-keys -t "${session}" Enter`);
|
|
152
|
-
return
|
|
161
|
+
return response(`Sent to ${session}`);
|
|
153
162
|
}
|
|
154
163
|
catch (e) {
|
|
155
|
-
return
|
|
164
|
+
return response(`Error: ${errorMessage(e)}`);
|
|
156
165
|
}
|
|
157
166
|
});
|
|
158
|
-
server.tool("kill", "Terminate a Claude tmux session and clean up resources.
|
|
167
|
+
server.tool("kill", "Terminate a Claude tmux session and clean up resources.", {
|
|
159
168
|
name: z.string().describe("Session name (as provided to spawn)"),
|
|
160
169
|
}, async ({ name }) => {
|
|
161
170
|
const session = sessionName(name);
|
|
171
|
+
if (!sessionExists(session)) {
|
|
172
|
+
return response(`Session '${name}' does not exist`);
|
|
173
|
+
}
|
|
162
174
|
try {
|
|
163
175
|
runTmux(`kill-session -t "${session}"`);
|
|
164
|
-
return
|
|
176
|
+
return response(`Killed ${session}`);
|
|
165
177
|
}
|
|
166
178
|
catch (e) {
|
|
167
|
-
return
|
|
179
|
+
return response(`Error: ${errorMessage(e)}`);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
server.tool("list", "List all active Claude tmux sessions.", {}, async () => {
|
|
183
|
+
const output = runTmuxSafe(`list-sessions -F "#{session_name}"`) ?? "";
|
|
184
|
+
const sessions = output.split('\n')
|
|
185
|
+
.filter(s => s.startsWith(SESSION_PREFIX))
|
|
186
|
+
.map(s => s.slice(SESSION_PREFIX.length));
|
|
187
|
+
if (sessions.length === 0) {
|
|
188
|
+
return response("No active sessions");
|
|
168
189
|
}
|
|
190
|
+
return response(sessions.join('\n'));
|
|
169
191
|
});
|
|
170
192
|
async function main() {
|
|
171
193
|
const transport = new StdioServerTransport();
|