dispatch-agents 0.4.1 → 0.5.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 +35 -0
- package/dist/cli.js +9 -3
- package/dist/mcp.js +263 -0
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -49,6 +49,41 @@ npm link
|
|
|
49
49
|
- `git` — for worktree management
|
|
50
50
|
- iTerm2 (recommended) — for native tab integration via `tmux -CC`
|
|
51
51
|
|
|
52
|
+
## MCP Server
|
|
53
|
+
|
|
54
|
+
Dispatch includes an MCP server so Claude Code can orchestrate agents directly — no shell commands needed.
|
|
55
|
+
|
|
56
|
+
### Setup
|
|
57
|
+
|
|
58
|
+
After installing dispatch, register the MCP server with Claude Code:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
claude mcp add --scope user dispatch node $(which dispatch-mcp)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
This exposes 6 tools to Claude Code:
|
|
65
|
+
|
|
66
|
+
| Tool | Description |
|
|
67
|
+
|------|-------------|
|
|
68
|
+
| `dispatch_run` | Launch an agent with a prompt (inline text, not a file) |
|
|
69
|
+
| `dispatch_list` | List all running agents with status |
|
|
70
|
+
| `dispatch_stop` | Stop a running agent |
|
|
71
|
+
| `dispatch_resume` | Resume a stopped agent |
|
|
72
|
+
| `dispatch_cleanup` | Remove worktrees and optionally branches |
|
|
73
|
+
| `dispatch_logs` | Get recent output from an agent |
|
|
74
|
+
|
|
75
|
+
### How it works
|
|
76
|
+
|
|
77
|
+
The MCP server wraps the dispatch CLI over stdio using the [Model Context Protocol](https://modelcontextprotocol.io). When Claude Code calls `dispatch_run`, the server writes the prompt to a temp file, runs `dispatch run --prompt-file <file>`, and cleans up. Interactive agents open iTerm tabs; headless agents run in the background.
|
|
78
|
+
|
|
79
|
+
### Working directory
|
|
80
|
+
|
|
81
|
+
By default the MCP server uses the directory Claude Code is running in. To override, set `DISPATCH_CWD`:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
claude mcp add --scope user dispatch -e DISPATCH_CWD=/path/to/repo node $(which dispatch-mcp)
|
|
85
|
+
```
|
|
86
|
+
|
|
52
87
|
## Usage
|
|
53
88
|
|
|
54
89
|
### Launch an agent
|
package/dist/cli.js
CHANGED
|
@@ -539,6 +539,7 @@ async function cmdRun(args, config) {
|
|
|
539
539
|
let extraArgs = "";
|
|
540
540
|
let skipWorktree = false;
|
|
541
541
|
let nameOverride = "";
|
|
542
|
+
let noAttach = false;
|
|
542
543
|
let i = 0;
|
|
543
544
|
while (i < args.length) {
|
|
544
545
|
const arg = args[i];
|
|
@@ -575,6 +576,10 @@ async function cmdRun(args, config) {
|
|
|
575
576
|
skipWorktree = true;
|
|
576
577
|
i++;
|
|
577
578
|
break;
|
|
579
|
+
case "--no-attach":
|
|
580
|
+
noAttach = true;
|
|
581
|
+
i++;
|
|
582
|
+
break;
|
|
578
583
|
case "--name":
|
|
579
584
|
case "-n":
|
|
580
585
|
nameOverride = args[++i];
|
|
@@ -614,7 +619,7 @@ async function cmdRun(args, config) {
|
|
|
614
619
|
await launchAgent(input, headless, extraArgs, skipWorktree, promptFile, nameOverride, config);
|
|
615
620
|
}
|
|
616
621
|
console.log();
|
|
617
|
-
if (!headless && inputs.length === 1) {
|
|
622
|
+
if (!headless && inputs.length === 1 && !noAttach) {
|
|
618
623
|
log.info("Attaching to tmux session...");
|
|
619
624
|
log.dim(" Detach with: Ctrl-B then D");
|
|
620
625
|
console.log();
|
|
@@ -703,10 +708,11 @@ function cmdStop(args) {
|
|
|
703
708
|
function cmdResume(args, config) {
|
|
704
709
|
const id = args[0];
|
|
705
710
|
if (!id) {
|
|
706
|
-
log.error("Usage: dispatch resume <agent-id> [--headless]");
|
|
711
|
+
log.error("Usage: dispatch resume <agent-id> [--headless] [--no-attach]");
|
|
707
712
|
process.exit(1);
|
|
708
713
|
}
|
|
709
714
|
const headless = args.includes("--headless") || args.includes("-H");
|
|
715
|
+
const noAttach = args.includes("--no-attach");
|
|
710
716
|
ensureTmux();
|
|
711
717
|
const wtPath = worktreePath(id, config);
|
|
712
718
|
if (!existsSync2(wtPath)) {
|
|
@@ -725,7 +731,7 @@ function cmdResume(args, config) {
|
|
|
725
731
|
`tmux send-keys -t "${tmuxTarget(id)}" "unset CLAUDECODE && claude --continue ${modelFlag}" Enter`
|
|
726
732
|
);
|
|
727
733
|
log.ok(`Resumed agent: ${id} (interactive)`);
|
|
728
|
-
tmuxAttach();
|
|
734
|
+
if (!noAttach) tmuxAttach();
|
|
729
735
|
} else {
|
|
730
736
|
const resumePrompt = "Continue working on the task.";
|
|
731
737
|
const claudeCmd = buildClaudeCmd(
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/mcp.ts
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import {
|
|
7
|
+
CallToolRequestSchema,
|
|
8
|
+
ListToolsRequestSchema
|
|
9
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
+
import { execSync } from "child_process";
|
|
11
|
+
import {
|
|
12
|
+
writeFileSync,
|
|
13
|
+
unlinkSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
existsSync
|
|
16
|
+
} from "fs";
|
|
17
|
+
import { tmpdir } from "os";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { randomBytes } from "crypto";
|
|
20
|
+
function stripAnsi(str) {
|
|
21
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "").replace(/\x1b\][^\x07]*\x07/g, "");
|
|
22
|
+
}
|
|
23
|
+
function dispatch(args) {
|
|
24
|
+
const cwd = process.env.DISPATCH_CWD || process.cwd();
|
|
25
|
+
try {
|
|
26
|
+
const output = execSync(`dispatch ${args}`, {
|
|
27
|
+
encoding: "utf-8",
|
|
28
|
+
cwd,
|
|
29
|
+
timeout: 12e4,
|
|
30
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
31
|
+
});
|
|
32
|
+
return stripAnsi(output).trim();
|
|
33
|
+
} catch (e) {
|
|
34
|
+
const out = [e.stdout, e.stderr, e.message].filter(Boolean).join("\n");
|
|
35
|
+
return stripAnsi(out).trim();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function makeTempPrompt(prompt) {
|
|
39
|
+
const name = `dispatch-mcp-${randomBytes(4).toString("hex")}.md`;
|
|
40
|
+
const path = join(tmpdir(), name);
|
|
41
|
+
writeFileSync(path, prompt);
|
|
42
|
+
return path;
|
|
43
|
+
}
|
|
44
|
+
var server = new Server(
|
|
45
|
+
{ name: "dispatch", version: "0.1.0" },
|
|
46
|
+
{ capabilities: { tools: {} } }
|
|
47
|
+
);
|
|
48
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
49
|
+
tools: [
|
|
50
|
+
{
|
|
51
|
+
name: "dispatch_run",
|
|
52
|
+
description: "Launch a Claude Code agent in an isolated git worktree. Pass the full task prompt inline. Returns agent ID and branch name.",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
type: "object",
|
|
55
|
+
properties: {
|
|
56
|
+
prompt: {
|
|
57
|
+
type: "string",
|
|
58
|
+
description: "Full task description/prompt for the agent"
|
|
59
|
+
},
|
|
60
|
+
ticket: {
|
|
61
|
+
type: "string",
|
|
62
|
+
description: "Linear ticket ID (e.g. HEY-907). Fetches title + description if LINEAR_API_KEY is set."
|
|
63
|
+
},
|
|
64
|
+
name: {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "Agent name and branch name (kebab-case). Defaults to ticket ID or derived from prompt."
|
|
67
|
+
},
|
|
68
|
+
headless: {
|
|
69
|
+
type: "boolean",
|
|
70
|
+
description: "Run in background (fire-and-forget). Default: false (interactive)."
|
|
71
|
+
},
|
|
72
|
+
model: {
|
|
73
|
+
type: "string",
|
|
74
|
+
description: "Claude model: sonnet, opus, haiku"
|
|
75
|
+
},
|
|
76
|
+
base_branch: {
|
|
77
|
+
type: "string",
|
|
78
|
+
description: "Branch to create worktree from. Default: dev."
|
|
79
|
+
},
|
|
80
|
+
max_turns: {
|
|
81
|
+
type: "number",
|
|
82
|
+
description: "Max agentic turns (headless only)"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
required: ["prompt"]
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "dispatch_list",
|
|
90
|
+
description: "List all running dispatch agents with their status (running/idle/exited).",
|
|
91
|
+
inputSchema: { type: "object", properties: {} }
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "dispatch_stop",
|
|
95
|
+
description: "Stop a running dispatch agent. Worktree and branch are preserved.",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
agent_id: { type: "string", description: "Agent ID to stop" }
|
|
100
|
+
},
|
|
101
|
+
required: ["agent_id"]
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "dispatch_resume",
|
|
106
|
+
description: "Resume a previously stopped agent. Picks up where it left off.",
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
agent_id: {
|
|
111
|
+
type: "string",
|
|
112
|
+
description: "Agent ID to resume"
|
|
113
|
+
},
|
|
114
|
+
headless: {
|
|
115
|
+
type: "boolean",
|
|
116
|
+
description: "Resume in headless mode"
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
required: ["agent_id"]
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "dispatch_cleanup",
|
|
124
|
+
description: "Remove an agent's worktree and optionally its branch.",
|
|
125
|
+
inputSchema: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {
|
|
128
|
+
agent_id: {
|
|
129
|
+
type: "string",
|
|
130
|
+
description: "Agent ID to clean up. Omit and set all=true for all agents."
|
|
131
|
+
},
|
|
132
|
+
all: {
|
|
133
|
+
type: "boolean",
|
|
134
|
+
description: "Clean up all worktrees"
|
|
135
|
+
},
|
|
136
|
+
delete_branch: {
|
|
137
|
+
type: "boolean",
|
|
138
|
+
description: "Also delete the git branch"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "dispatch_logs",
|
|
145
|
+
description: "Get recent output from a dispatch agent (log file or tmux capture).",
|
|
146
|
+
inputSchema: {
|
|
147
|
+
type: "object",
|
|
148
|
+
properties: {
|
|
149
|
+
agent_id: { type: "string", description: "Agent ID" },
|
|
150
|
+
lines: {
|
|
151
|
+
type: "number",
|
|
152
|
+
description: "Number of lines to return. Default: 50."
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
required: ["agent_id"]
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}));
|
|
160
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
161
|
+
const { name, arguments: args = {} } = request.params;
|
|
162
|
+
switch (name) {
|
|
163
|
+
case "dispatch_run": {
|
|
164
|
+
const {
|
|
165
|
+
prompt,
|
|
166
|
+
ticket,
|
|
167
|
+
name: agentName,
|
|
168
|
+
headless,
|
|
169
|
+
model,
|
|
170
|
+
base_branch,
|
|
171
|
+
max_turns
|
|
172
|
+
} = args;
|
|
173
|
+
const tmpFile = makeTempPrompt(prompt);
|
|
174
|
+
try {
|
|
175
|
+
const parts = ["run"];
|
|
176
|
+
parts.push(ticket || "prompt-file");
|
|
177
|
+
parts.push(`--prompt-file "${tmpFile}"`);
|
|
178
|
+
if (agentName) parts.push(`--name "${agentName}"`);
|
|
179
|
+
if (headless) parts.push("--headless");
|
|
180
|
+
if (model) parts.push(`--model ${model}`);
|
|
181
|
+
if (base_branch) parts.push(`--base ${base_branch}`);
|
|
182
|
+
if (max_turns) parts.push(`--max-turns ${max_turns}`);
|
|
183
|
+
if (headless) parts.push("--no-attach");
|
|
184
|
+
const output = dispatch(parts.join(" "));
|
|
185
|
+
return { content: [{ type: "text", text: output }] };
|
|
186
|
+
} finally {
|
|
187
|
+
try {
|
|
188
|
+
unlinkSync(tmpFile);
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
case "dispatch_list": {
|
|
194
|
+
const output = dispatch("list");
|
|
195
|
+
return {
|
|
196
|
+
content: [{ type: "text", text: output || "No agents running." }]
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
case "dispatch_stop": {
|
|
200
|
+
const { agent_id } = args;
|
|
201
|
+
const output = dispatch(`stop ${agent_id}`);
|
|
202
|
+
return { content: [{ type: "text", text: output }] };
|
|
203
|
+
}
|
|
204
|
+
case "dispatch_resume": {
|
|
205
|
+
const { agent_id, headless } = args;
|
|
206
|
+
const flags = headless ? "--headless" : "";
|
|
207
|
+
const output = dispatch(
|
|
208
|
+
`resume ${agent_id} ${flags} --no-attach`.trim()
|
|
209
|
+
);
|
|
210
|
+
return { content: [{ type: "text", text: output }] };
|
|
211
|
+
}
|
|
212
|
+
case "dispatch_cleanup": {
|
|
213
|
+
const { agent_id, all, delete_branch } = args;
|
|
214
|
+
const parts = ["cleanup"];
|
|
215
|
+
if (all) parts.push("--all");
|
|
216
|
+
else if (agent_id) parts.push(agent_id);
|
|
217
|
+
if (delete_branch) parts.push("--delete-branch");
|
|
218
|
+
const output = dispatch(parts.join(" "));
|
|
219
|
+
return { content: [{ type: "text", text: output }] };
|
|
220
|
+
}
|
|
221
|
+
case "dispatch_logs": {
|
|
222
|
+
const { agent_id, lines = 50 } = args;
|
|
223
|
+
const cwd = process.env.DISPATCH_CWD || process.cwd();
|
|
224
|
+
const logPath = join(cwd, ".worktrees", agent_id, ".dispatch.log");
|
|
225
|
+
if (existsSync(logPath)) {
|
|
226
|
+
const content = readFileSync(logPath, "utf-8");
|
|
227
|
+
const logLines = content.split("\n");
|
|
228
|
+
const lastLines = logLines.slice(-lines).join("\n");
|
|
229
|
+
return {
|
|
230
|
+
content: [{ type: "text", text: stripAnsi(lastLines) }]
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const output = execSync(
|
|
235
|
+
`tmux capture-pane -t "dispatch:${agent_id}" -p -S -${lines}`,
|
|
236
|
+
{ encoding: "utf-8", timeout: 5e3 }
|
|
237
|
+
);
|
|
238
|
+
return {
|
|
239
|
+
content: [{ type: "text", text: stripAnsi(output).trim() }]
|
|
240
|
+
};
|
|
241
|
+
} catch {
|
|
242
|
+
return {
|
|
243
|
+
content: [
|
|
244
|
+
{
|
|
245
|
+
type: "text",
|
|
246
|
+
text: `Agent '${agent_id}' not found or no output available.`
|
|
247
|
+
}
|
|
248
|
+
]
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
default:
|
|
253
|
+
return {
|
|
254
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
255
|
+
isError: true
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
async function main() {
|
|
260
|
+
const transport = new StdioServerTransport();
|
|
261
|
+
await server.connect(transport);
|
|
262
|
+
}
|
|
263
|
+
main().catch(console.error);
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dispatch-agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Orchestrate Claude Code agents in git worktrees",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"dispatch": "./dist/cli.js"
|
|
7
|
+
"dispatch": "./dist/cli.js",
|
|
8
|
+
"dispatch-mcp": "./dist/mcp.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"build": "tsup",
|
|
@@ -26,6 +27,9 @@
|
|
|
26
27
|
"type": "git",
|
|
27
28
|
"url": "https://github.com/paperMoose/dispatch.git"
|
|
28
29
|
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.12.1"
|
|
32
|
+
},
|
|
29
33
|
"devDependencies": {
|
|
30
34
|
"tsup": "^8.0.0",
|
|
31
35
|
"tsx": "^4.0.0",
|