claude-tmux 1.0.0
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 +83 -0
- package/dist/index.js +179 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# claude-tmux
|
|
2
|
+
|
|
3
|
+
MCP server for orchestrating multiple Claude Code instances via tmux.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Claude Code MCP settings:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"claude-tmux": {
|
|
13
|
+
"command": "npx",
|
|
14
|
+
"args": ["-y", "claude-tmux"]
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Requirements
|
|
21
|
+
|
|
22
|
+
- [tmux](https://github.com/tmux/tmux) must be installed
|
|
23
|
+
- [Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code) must be installed
|
|
24
|
+
|
|
25
|
+
## Tools
|
|
26
|
+
|
|
27
|
+
### spawn
|
|
28
|
+
|
|
29
|
+
Launch a new Claude Code instance in a tmux session.
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
spawn("task-name", workdir, prompt)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- `name`: Unique session name (e.g., 'refactor-auth', 'debug-api')
|
|
36
|
+
- `workdir`: Working directory for Claude to operate in
|
|
37
|
+
- `prompt`: Initial prompt to send to Claude on startup
|
|
38
|
+
- `dangerouslySkipPermissions`: Skip permission prompts for fully autonomous operation
|
|
39
|
+
|
|
40
|
+
### read
|
|
41
|
+
|
|
42
|
+
Wait for a Claude session to finish working and return the terminal output.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
read("task-name")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### send
|
|
49
|
+
|
|
50
|
+
Send a follow-up message to a running Claude session.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
send("task-name", "do something else")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### kill
|
|
57
|
+
|
|
58
|
+
Terminate a Claude tmux session and clean up resources.
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
kill("task-name")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage Pattern
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
spawn("task-name", workdir, prompt) → starts session
|
|
68
|
+
read("task-name") → waits for completion, returns output
|
|
69
|
+
kill("task-name") → cleanup
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
For steering mid-task:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
send("task-name", "do something else")
|
|
76
|
+
read("task-name") → waits for completion, returns output
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Tips
|
|
80
|
+
|
|
81
|
+
- Use `dangerouslySkipPermissions: true` for fully autonomous operation
|
|
82
|
+
- User can attach manually: `tmux attach -t claude-<name>`
|
|
83
|
+
- Always kill sessions when done to avoid orphaned processes
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import { writeFileSync } from "fs";
|
|
7
|
+
const SESSION_PREFIX = "claude-";
|
|
8
|
+
function runTmux(args) {
|
|
9
|
+
return execSync(`tmux ${args}`, { encoding: "utf-8", timeout: 10000 }).trim();
|
|
10
|
+
}
|
|
11
|
+
function runTmuxSafe(args) {
|
|
12
|
+
try {
|
|
13
|
+
return runTmux(args);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function sessionName(name) {
|
|
20
|
+
return `${SESSION_PREFIX}${name.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
|
|
21
|
+
}
|
|
22
|
+
function sleep(ms) {
|
|
23
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
24
|
+
}
|
|
25
|
+
function isClaudeIdle(output) {
|
|
26
|
+
if (output.includes('esc to interrupt')) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
function filterUIChrome(output) {
|
|
32
|
+
const lines = output.split('\n');
|
|
33
|
+
const filtered = lines.filter(line => {
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
if (/^[─━\-]{10,}$/.test(trimmed))
|
|
36
|
+
return false;
|
|
37
|
+
if (trimmed.startsWith('> Try "'))
|
|
38
|
+
return false;
|
|
39
|
+
if (trimmed.includes('bypass permissions on'))
|
|
40
|
+
return false;
|
|
41
|
+
if (trimmed.includes('↵ send'))
|
|
42
|
+
return false;
|
|
43
|
+
if (trimmed.includes('shift+tab to cycle'))
|
|
44
|
+
return false;
|
|
45
|
+
return true;
|
|
46
|
+
});
|
|
47
|
+
return filtered.join('\n').trim();
|
|
48
|
+
}
|
|
49
|
+
async function waitForIdle(session) {
|
|
50
|
+
const timeout = 600000; // 10 minutes
|
|
51
|
+
const lines = 50;
|
|
52
|
+
const startTime = Date.now();
|
|
53
|
+
let lastOutput = "";
|
|
54
|
+
let stableCount = 0;
|
|
55
|
+
while (Date.now() - startTime < timeout) {
|
|
56
|
+
await sleep(2000);
|
|
57
|
+
try {
|
|
58
|
+
const output = runTmux(`capture-pane -t "${session}" -p -S -${lines}`);
|
|
59
|
+
if (isClaudeIdle(output)) {
|
|
60
|
+
return filterUIChrome(output);
|
|
61
|
+
}
|
|
62
|
+
if (output === lastOutput) {
|
|
63
|
+
stableCount++;
|
|
64
|
+
if (stableCount >= 3) {
|
|
65
|
+
return filterUIChrome(output);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
stableCount = 0;
|
|
70
|
+
lastOutput = output;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
return `Error: ${e.message}`;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return `Timeout after 10 minutes. Session still running.\n\n${filterUIChrome(lastOutput)}`;
|
|
78
|
+
}
|
|
79
|
+
const server = new McpServer({
|
|
80
|
+
name: "claude-tmux",
|
|
81
|
+
version: "1.0.0",
|
|
82
|
+
}, {
|
|
83
|
+
instructions: `# claude-tmux: Autonomous Claude Agents
|
|
84
|
+
|
|
85
|
+
Spawn Claude Code instances in tmux sessions for long-running, independent tasks.
|
|
86
|
+
|
|
87
|
+
## Tools
|
|
88
|
+
- **spawn**: Start a new Claude session with a prompt.
|
|
89
|
+
- **read**: Wait for a session to finish and return the output. You can continue other work while waiting.
|
|
90
|
+
- **send**: Send a follow-up message to steer a running session mid-task.
|
|
91
|
+
- **kill**: Terminate a session and clean up resources.
|
|
92
|
+
|
|
93
|
+
## Pattern
|
|
94
|
+
|
|
95
|
+
\`\`\`
|
|
96
|
+
spawn("task-name", workdir, prompt) → starts session
|
|
97
|
+
read("task-name") → waits for completion, returns output
|
|
98
|
+
kill("task-name") → cleanup
|
|
99
|
+
\`\`\`
|
|
100
|
+
|
|
101
|
+
For steering mid-task:
|
|
102
|
+
\`\`\`
|
|
103
|
+
send("task-name", "do something else")
|
|
104
|
+
read("task-name") → waits for completion, returns output
|
|
105
|
+
\`\`\`
|
|
106
|
+
|
|
107
|
+
## Tips
|
|
108
|
+
- Use \`dangerouslySkipPermissions: true\` for fully autonomous operation
|
|
109
|
+
- User can attach manually: \`tmux attach -t claude-<name>\`
|
|
110
|
+
- Always kill sessions when done`,
|
|
111
|
+
});
|
|
112
|
+
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.", {
|
|
113
|
+
name: z.string().min(1).max(50).describe("Unique session name (e.g., 'refactor-auth', 'debug-api')"),
|
|
114
|
+
workdir: z.string().describe("Working directory for Claude to operate in"),
|
|
115
|
+
prompt: z.string().optional().describe("Initial prompt to send to Claude on startup"),
|
|
116
|
+
dangerouslySkipPermissions: z.boolean().optional().default(false).describe("Skip permission prompts for fully autonomous operation"),
|
|
117
|
+
}, async ({ name, workdir, prompt, dangerouslySkipPermissions }) => {
|
|
118
|
+
const session = sessionName(name);
|
|
119
|
+
runTmuxSafe(`kill-session -t "${session}"`);
|
|
120
|
+
try {
|
|
121
|
+
runTmux(`new-session -d -s "${session}" -c "${workdir}"`);
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
125
|
+
}
|
|
126
|
+
const flags = dangerouslySkipPermissions ? "--dangerously-skip-permissions " : "";
|
|
127
|
+
if (prompt) {
|
|
128
|
+
const tempFile = `/tmp/claude-prompt-${session}.txt`;
|
|
129
|
+
writeFileSync(tempFile, prompt);
|
|
130
|
+
runTmux(`send-keys -t "${session}" 'claude ${flags}"$(cat ${tempFile})" && rm ${tempFile}' Enter`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
runTmux(`send-keys -t "${session}" 'claude ${flags}' Enter`);
|
|
134
|
+
}
|
|
135
|
+
return { content: [{ type: "text", text: `Started ${session}` }] };
|
|
136
|
+
});
|
|
137
|
+
server.tool("read", "Wait for a Claude session to finish working and return the terminal output. You can continue other work while waiting.", {
|
|
138
|
+
name: z.string().describe("Session name (as provided to spawn)"),
|
|
139
|
+
}, async ({ name }) => {
|
|
140
|
+
const session = sessionName(name);
|
|
141
|
+
const output = await waitForIdle(session);
|
|
142
|
+
return { content: [{ type: "text", text: output }] };
|
|
143
|
+
});
|
|
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.", {
|
|
145
|
+
name: z.string().describe("Session name (as provided to spawn)"),
|
|
146
|
+
text: z.string().describe("Message to send to Claude"),
|
|
147
|
+
}, async ({ name, text }) => {
|
|
148
|
+
const session = sessionName(name);
|
|
149
|
+
try {
|
|
150
|
+
const escaped = text
|
|
151
|
+
.replace(/\\/g, '\\\\')
|
|
152
|
+
.replace(/"/g, '\\"')
|
|
153
|
+
.replace(/\$/g, '\\$')
|
|
154
|
+
.replace(/`/g, '\\`');
|
|
155
|
+
runTmux(`send-keys -t "${session}" -l "${escaped}"`);
|
|
156
|
+
runTmux(`send-keys -t "${session}" Enter`);
|
|
157
|
+
return { content: [{ type: "text", text: `Sent to ${session}` }] };
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
server.tool("kill", "Terminate a Claude tmux session and clean up resources. Always kill sessions when done to avoid orphaned processes.", {
|
|
164
|
+
name: z.string().describe("Session name (as provided to spawn)"),
|
|
165
|
+
}, async ({ name }) => {
|
|
166
|
+
const session = sessionName(name);
|
|
167
|
+
try {
|
|
168
|
+
runTmux(`kill-session -t "${session}"`);
|
|
169
|
+
return { content: [{ type: "text", text: `Killed ${session}` }] };
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
return { content: [{ type: "text", text: `Error: ${e.message}` }] };
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
async function main() {
|
|
176
|
+
const transport = new StdioServerTransport();
|
|
177
|
+
await server.connect(transport);
|
|
178
|
+
}
|
|
179
|
+
main().catch(console.error);
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-tmux",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for orchestrating multiple Claude Code instances via tmux",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"claude-tmux": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"prepublishOnly": "npm run build"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"claude",
|
|
19
|
+
"mcp",
|
|
20
|
+
"tmux",
|
|
21
|
+
"ai",
|
|
22
|
+
"agent",
|
|
23
|
+
"anthropic",
|
|
24
|
+
"claude-code"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/Ilm-Alan/claude-tmux.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/Ilm-Alan/claude-tmux/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/Ilm-Alan/claude-tmux#readme",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
36
|
+
"zod": "^3.23.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/node": "^20.0.0",
|
|
40
|
+
"typescript": "^5.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|