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.
Files changed (3) hide show
  1. package/README.md +9 -16
  2. package/dist/index.js +60 -51
  3. 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("task-name", workdir, prompt)
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("task-name", workdir, prompt) starts session
70
- read("task-name") waits for completion, returns output
71
- kill("task-name") cleanup
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
- - Use `dangerouslySkipPermissions: true` for fully autonomous operation
84
- - User can attach manually: `tmux attach -t claude-<name>`
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(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.8",
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 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("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
- - Use \`dangerouslySkipPermissions: true\` for fully autonomous operation
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
- prompt: z.string().optional().describe("Initial prompt to send to Claude on startup"),
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 { content: [{ type: "text", text: `Error: ${e.message}` }] };
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
- else {
140
- runTmux(`send-keys -t "${session}" 'claude ${flags}' Enter`);
141
- }
142
- return { content: [{ type: "text", text: `Started ${session}` }] };
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 { content: [{ type: "text", text: output }] };
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 { content: [{ type: "text", text: `Sent to ${session}` }] };
160
+ return response(`Sent to ${session}`);
165
161
  }
166
162
  catch (e) {
167
- return { content: [{ type: "text", text: `Error: ${e.message}` }] };
163
+ return response(`Error: ${errorMessage(e)}`);
168
164
  }
169
165
  });
170
- 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.", {
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 { content: [{ type: "text", text: `Killed ${session}` }] };
175
+ return response(`Killed ${session}`);
177
176
  }
178
177
  catch (e) {
179
- 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");
180
188
  }
189
+ return response(sessions.join('\n'));
181
190
  });
182
191
  async function main() {
183
192
  const transport = new StdioServerTransport();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-tmux",
3
- "version": "1.0.8",
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",