claude-tmux 1.0.8 → 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 +9 -16
- package/dist/index.js +60 -51
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,13 +31,12 @@ Add to your Claude Code MCP settings:
|
|
|
31
31
|
Launch a new Claude Code instance in a tmux session.
|
|
32
32
|
|
|
33
33
|
```
|
|
34
|
-
spawn(
|
|
34
|
+
spawn(name, prompt, workdir)
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
- `name`: Unique session name (e.g., 'refactor-auth', 'debug-api')
|
|
38
|
+
- `prompt`: Initial prompt to send to Claude
|
|
38
39
|
- `workdir`: Working directory for Claude to operate in
|
|
39
|
-
- `prompt`: Initial prompt to send to Claude on startup
|
|
40
|
-
- `dangerouslySkipPermissions`: Skip permission prompts for fully autonomous operation
|
|
41
40
|
|
|
42
41
|
### read
|
|
43
42
|
|
|
@@ -66,20 +65,14 @@ kill("task-name")
|
|
|
66
65
|
## Usage Pattern
|
|
67
66
|
|
|
68
67
|
```
|
|
69
|
-
spawn(
|
|
70
|
-
read(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
For steering mid-task:
|
|
75
|
-
|
|
76
|
-
```
|
|
77
|
-
send("task-name", "do something else")
|
|
78
|
-
read("task-name") → waits for completion, returns output
|
|
68
|
+
spawn(name, prompt, workdir) → start session
|
|
69
|
+
read(name) → wait for completion, get output
|
|
70
|
+
send(name, text) → steer with follow-up
|
|
71
|
+
read(name) → wait for completion, get output
|
|
72
|
+
kill(name) → cleanup
|
|
79
73
|
```
|
|
80
74
|
|
|
81
75
|
## Tips
|
|
82
76
|
|
|
83
|
-
-
|
|
84
|
-
-
|
|
85
|
-
- Always kill sessions when done to avoid orphaned processes
|
|
77
|
+
- Verify output shows task completion before killing. Idle agents are fine to leave running.
|
|
78
|
+
- Attach manually: `tmux attach -t claude-<name>`
|
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,66 +105,50 @@ 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("task-name", workdir, prompt) → starts session
|
|
104
|
-
read("task-name") → waits for completion, returns output
|
|
105
|
-
kill("task-name") → cleanup
|
|
106
|
-
\`\`\`
|
|
107
|
-
|
|
108
|
-
For steering mid-task:
|
|
109
|
-
\`\`\`
|
|
110
|
-
send("task-name", "do something else")
|
|
111
|
-
read("task-name") → waits for completion, returns output
|
|
112
|
-
\`\`\`
|
|
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.
|
|
113
111
|
|
|
114
112
|
## Tips
|
|
115
|
-
-
|
|
116
|
-
- User can attach manually: \`tmux attach -t claude-<name
|
|
117
|
-
- Always kill sessions when done`,
|
|
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>\``,
|
|
118
115
|
});
|
|
119
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.", {
|
|
120
117
|
name: z.string().min(1).max(50).describe("Unique session name (e.g., 'refactor-auth', 'debug-api')"),
|
|
118
|
+
prompt: z.string().describe("Initial prompt to send to Claude on startup"),
|
|
121
119
|
workdir: z.string().describe("Working directory for Claude to operate in"),
|
|
122
|
-
|
|
123
|
-
dangerouslySkipPermissions: z.boolean().optional().default(false).describe("Skip permission prompts for fully autonomous operation"),
|
|
124
|
-
}, async ({ name, workdir, prompt, dangerouslySkipPermissions }) => {
|
|
120
|
+
}, async ({ name, prompt, workdir }) => {
|
|
125
121
|
const session = sessionName(name);
|
|
126
122
|
runTmuxSafe(`kill-session -t "${session}"`);
|
|
127
123
|
try {
|
|
128
124
|
runTmux(`new-session -d -s "${session}" -c "${workdir}"`);
|
|
129
125
|
}
|
|
130
126
|
catch (e) {
|
|
131
|
-
return
|
|
132
|
-
}
|
|
133
|
-
const flags = dangerouslySkipPermissions ? "--dangerously-skip-permissions " : "";
|
|
134
|
-
if (prompt) {
|
|
135
|
-
const tempFile = `/tmp/claude-prompt-${session}.txt`;
|
|
136
|
-
writeFileSync(tempFile, prompt);
|
|
137
|
-
runTmux(`send-keys -t "${session}" 'claude ${flags}"$(cat ${tempFile})" && rm ${tempFile}' Enter`);
|
|
127
|
+
return response(`Error: ${errorMessage(e)}`);
|
|
138
128
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
return
|
|
129
|
+
const tempFile = `/tmp/claude-prompt-${session}.txt`;
|
|
130
|
+
writeFileSync(tempFile, prompt);
|
|
131
|
+
runTmux(`send-keys -t "${session}" 'claude --dangerously-skip-permissions "$(cat ${tempFile})" && rm ${tempFile}' Enter`);
|
|
132
|
+
return response(`Started ${session}`);
|
|
143
133
|
});
|
|
144
134
|
server.tool("read", "Wait for a Claude session to finish working and return the terminal output. You can continue other work while waiting.", {
|
|
145
135
|
name: z.string().describe("Session name (as provided to spawn)"),
|
|
146
136
|
}, async ({ name }) => {
|
|
147
137
|
const session = sessionName(name);
|
|
138
|
+
if (!sessionExists(session)) {
|
|
139
|
+
return response(`Session '${name}' does not exist`);
|
|
140
|
+
}
|
|
148
141
|
const output = await waitForIdle(session);
|
|
149
|
-
return
|
|
142
|
+
return response(output);
|
|
150
143
|
});
|
|
151
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.", {
|
|
152
145
|
name: z.string().describe("Session name (as provided to spawn)"),
|
|
153
146
|
text: z.string().describe("Message to send to Claude"),
|
|
154
147
|
}, async ({ name, text }) => {
|
|
155
148
|
const session = sessionName(name);
|
|
149
|
+
if (!sessionExists(session)) {
|
|
150
|
+
return response(`Session '${name}' does not exist`);
|
|
151
|
+
}
|
|
156
152
|
try {
|
|
157
153
|
const escaped = text
|
|
158
154
|
.replace(/\\/g, '\\\\')
|
|
@@ -161,23 +157,36 @@ server.tool("send", "Send a follow-up message to a running Claude session. Use t
|
|
|
161
157
|
.replace(/`/g, '\\`');
|
|
162
158
|
runTmux(`send-keys -t "${session}" -l "${escaped}"`);
|
|
163
159
|
runTmux(`send-keys -t "${session}" Enter`);
|
|
164
|
-
return
|
|
160
|
+
return response(`Sent to ${session}`);
|
|
165
161
|
}
|
|
166
162
|
catch (e) {
|
|
167
|
-
return
|
|
163
|
+
return response(`Error: ${errorMessage(e)}`);
|
|
168
164
|
}
|
|
169
165
|
});
|
|
170
|
-
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.", {
|
|
171
167
|
name: z.string().describe("Session name (as provided to spawn)"),
|
|
172
168
|
}, async ({ name }) => {
|
|
173
169
|
const session = sessionName(name);
|
|
170
|
+
if (!sessionExists(session)) {
|
|
171
|
+
return response(`Session '${name}' does not exist`);
|
|
172
|
+
}
|
|
174
173
|
try {
|
|
175
174
|
runTmux(`kill-session -t "${session}"`);
|
|
176
|
-
return
|
|
175
|
+
return response(`Killed ${session}`);
|
|
177
176
|
}
|
|
178
177
|
catch (e) {
|
|
179
|
-
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");
|
|
180
188
|
}
|
|
189
|
+
return response(sessions.join('\n'));
|
|
181
190
|
});
|
|
182
191
|
async function main() {
|
|
183
192
|
const transport = new StdioServerTransport();
|