claude-tmux 1.0.9 → 1.0.11

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