claude-tmux 1.0.9 → 1.0.10
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 +1 -1
- package/dist/index.js +55 -34
- package/package.json +1 -1
package/README.md
CHANGED
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,13 @@ 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
|
-
- **kill**: Terminate a session
|
|
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
|
+
- **kill**: Terminate a session.
|
|
109
111
|
|
|
110
112
|
## Tips
|
|
111
|
-
-
|
|
112
|
-
-
|
|
113
|
+
- Verify output shows task completion before killing. Idle agents are fine to leave running.
|
|
114
|
+
- User can attach manually: \`tmux attach -t claude-<name>\``,
|
|
113
115
|
});
|
|
114
116
|
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
117
|
name: z.string().min(1).max(50).describe("Unique session name (e.g., 'refactor-auth', 'debug-api')"),
|
|
@@ -122,25 +124,31 @@ server.tool("spawn", "Launch a new Claude Code instance in a tmux session. Creat
|
|
|
122
124
|
runTmux(`new-session -d -s "${session}" -c "${workdir}"`);
|
|
123
125
|
}
|
|
124
126
|
catch (e) {
|
|
125
|
-
return
|
|
127
|
+
return response(`Error: ${errorMessage(e)}`);
|
|
126
128
|
}
|
|
127
129
|
const tempFile = `/tmp/claude-prompt-${session}.txt`;
|
|
128
130
|
writeFileSync(tempFile, prompt);
|
|
129
131
|
runTmux(`send-keys -t "${session}" 'claude --dangerously-skip-permissions "$(cat ${tempFile})" && rm ${tempFile}' Enter`);
|
|
130
|
-
return
|
|
132
|
+
return response(`Started ${session}`);
|
|
131
133
|
});
|
|
132
134
|
server.tool("read", "Wait for a Claude session to finish working and return the terminal output. You can continue other work while waiting.", {
|
|
133
135
|
name: z.string().describe("Session name (as provided to spawn)"),
|
|
134
136
|
}, async ({ name }) => {
|
|
135
137
|
const session = sessionName(name);
|
|
138
|
+
if (!sessionExists(session)) {
|
|
139
|
+
return response(`Session '${name}' does not exist`);
|
|
140
|
+
}
|
|
136
141
|
const output = await waitForIdle(session);
|
|
137
|
-
return
|
|
142
|
+
return response(output);
|
|
138
143
|
});
|
|
139
144
|
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
145
|
name: z.string().describe("Session name (as provided to spawn)"),
|
|
141
146
|
text: z.string().describe("Message to send to Claude"),
|
|
142
147
|
}, async ({ name, text }) => {
|
|
143
148
|
const session = sessionName(name);
|
|
149
|
+
if (!sessionExists(session)) {
|
|
150
|
+
return response(`Session '${name}' does not exist`);
|
|
151
|
+
}
|
|
144
152
|
try {
|
|
145
153
|
const escaped = text
|
|
146
154
|
.replace(/\\/g, '\\\\')
|
|
@@ -149,23 +157,36 @@ server.tool("send", "Send a follow-up message to a running Claude session. Use t
|
|
|
149
157
|
.replace(/`/g, '\\`');
|
|
150
158
|
runTmux(`send-keys -t "${session}" -l "${escaped}"`);
|
|
151
159
|
runTmux(`send-keys -t "${session}" Enter`);
|
|
152
|
-
return
|
|
160
|
+
return response(`Sent to ${session}`);
|
|
153
161
|
}
|
|
154
162
|
catch (e) {
|
|
155
|
-
return
|
|
163
|
+
return response(`Error: ${errorMessage(e)}`);
|
|
156
164
|
}
|
|
157
165
|
});
|
|
158
|
-
server.tool("kill", "Terminate a Claude tmux session and clean up resources.
|
|
166
|
+
server.tool("kill", "Terminate a Claude tmux session and clean up resources.", {
|
|
159
167
|
name: z.string().describe("Session name (as provided to spawn)"),
|
|
160
168
|
}, async ({ name }) => {
|
|
161
169
|
const session = sessionName(name);
|
|
170
|
+
if (!sessionExists(session)) {
|
|
171
|
+
return response(`Session '${name}' does not exist`);
|
|
172
|
+
}
|
|
162
173
|
try {
|
|
163
174
|
runTmux(`kill-session -t "${session}"`);
|
|
164
|
-
return
|
|
175
|
+
return response(`Killed ${session}`);
|
|
165
176
|
}
|
|
166
177
|
catch (e) {
|
|
167
|
-
return
|
|
178
|
+
return response(`Error: ${errorMessage(e)}`);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
server.tool("list", "List all active Claude tmux sessions.", {}, async () => {
|
|
182
|
+
const output = runTmuxSafe(`list-sessions -F "#{session_name}"`) ?? "";
|
|
183
|
+
const sessions = output.split('\n')
|
|
184
|
+
.filter(s => s.startsWith(SESSION_PREFIX))
|
|
185
|
+
.map(s => s.slice(SESSION_PREFIX.length));
|
|
186
|
+
if (sessions.length === 0) {
|
|
187
|
+
return response("No active sessions");
|
|
168
188
|
}
|
|
189
|
+
return response(sessions.join('\n'));
|
|
169
190
|
});
|
|
170
191
|
async function main() {
|
|
171
192
|
const transport = new StdioServerTransport();
|