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.
Files changed (3) hide show
  1. package/README.md +83 -0
  2. package/dist/index.js +179 -0
  3. 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
+ }