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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/index.js +55 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -74,5 +74,5 @@ kill(name) → cleanup
74
74
 
75
75
  ## Tips
76
76
 
77
+ - Verify output shows task completion before killing. Idle agents are fine to leave running.
77
78
  - 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(5000);
60
- while (Date.now() - startTime < timeout) {
61
- await sleep(2000);
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 -${lines}`);
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 >= 2) {
88
+ if (idleCount >= IDLE_THRESHOLD) {
77
89
  return filterUIChrome(output);
78
90
  }
79
91
  }
80
92
  catch (e) {
81
- return `Error: ${e.message}`;
93
+ return `Error: ${errorMessage(e)}`;
82
94
  }
83
95
  }
84
- return `Timeout after 10 minutes. Session still running.\n\n${filterUIChrome(output)}`;
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.9",
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 the output. You can continue other work while waiting.
97
- - **send**: Send a follow-up message to steer a running session mid-task.
98
- - **kill**: Terminate a session and clean up resources.
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
- - User can attach manually: \`tmux attach -t claude-<name>\`
112
- - 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>\``,
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 { content: [{ type: "text", text: `Error: ${e.message}` }] };
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 { content: [{ type: "text", text: `Started ${session}` }] };
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 { content: [{ type: "text", text: output }] };
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 { content: [{ type: "text", text: `Sent to ${session}` }] };
160
+ return response(`Sent to ${session}`);
153
161
  }
154
162
  catch (e) {
155
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
163
+ return response(`Error: ${errorMessage(e)}`);
156
164
  }
157
165
  });
158
- server.tool("kill", "Terminate a Claude tmux session and clean up resources. Always kill sessions when done to avoid orphaned processes.", {
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 { content: [{ type: "text", text: `Killed ${session}` }] };
175
+ return response(`Killed ${session}`);
165
176
  }
166
177
  catch (e) {
167
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-tmux",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "MCP server for orchestrating multiple Claude Code instances via tmux",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",