swarm-code 0.1.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/LICENSE +21 -0
- package/README.md +384 -0
- package/bin/swarm.mjs +45 -0
- package/dist/agents/aider.d.ts +12 -0
- package/dist/agents/aider.js +182 -0
- package/dist/agents/claude-code.d.ts +9 -0
- package/dist/agents/claude-code.js +216 -0
- package/dist/agents/codex.d.ts +14 -0
- package/dist/agents/codex.js +193 -0
- package/dist/agents/direct-llm.d.ts +9 -0
- package/dist/agents/direct-llm.js +78 -0
- package/dist/agents/mock.d.ts +9 -0
- package/dist/agents/mock.js +77 -0
- package/dist/agents/opencode.d.ts +23 -0
- package/dist/agents/opencode.js +571 -0
- package/dist/agents/provider.d.ts +11 -0
- package/dist/agents/provider.js +31 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.js +285 -0
- package/dist/compression/compressor.d.ts +28 -0
- package/dist/compression/compressor.js +265 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.js +170 -0
- package/dist/core/repl.d.ts +69 -0
- package/dist/core/repl.js +336 -0
- package/dist/core/rlm.d.ts +63 -0
- package/dist/core/rlm.js +409 -0
- package/dist/core/runtime.py +335 -0
- package/dist/core/types.d.ts +131 -0
- package/dist/core/types.js +19 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.js +75 -0
- package/dist/interactive-swarm.d.ts +20 -0
- package/dist/interactive-swarm.js +1041 -0
- package/dist/interactive.d.ts +10 -0
- package/dist/interactive.js +1765 -0
- package/dist/main.d.ts +15 -0
- package/dist/main.js +242 -0
- package/dist/mcp/server.d.ts +15 -0
- package/dist/mcp/server.js +72 -0
- package/dist/mcp/session.d.ts +73 -0
- package/dist/mcp/session.js +184 -0
- package/dist/mcp/tools.d.ts +15 -0
- package/dist/mcp/tools.js +377 -0
- package/dist/memory/episodic.d.ts +132 -0
- package/dist/memory/episodic.js +390 -0
- package/dist/prompts/orchestrator.d.ts +5 -0
- package/dist/prompts/orchestrator.js +191 -0
- package/dist/routing/model-router.d.ts +130 -0
- package/dist/routing/model-router.js +515 -0
- package/dist/swarm.d.ts +14 -0
- package/dist/swarm.js +557 -0
- package/dist/threads/cache.d.ts +58 -0
- package/dist/threads/cache.js +198 -0
- package/dist/threads/manager.d.ts +85 -0
- package/dist/threads/manager.js +659 -0
- package/dist/ui/banner.d.ts +14 -0
- package/dist/ui/banner.js +42 -0
- package/dist/ui/dashboard.d.ts +33 -0
- package/dist/ui/dashboard.js +151 -0
- package/dist/ui/index.d.ts +10 -0
- package/dist/ui/index.js +11 -0
- package/dist/ui/log.d.ts +39 -0
- package/dist/ui/log.js +126 -0
- package/dist/ui/onboarding.d.ts +14 -0
- package/dist/ui/onboarding.js +518 -0
- package/dist/ui/spinner.d.ts +25 -0
- package/dist/ui/spinner.js +113 -0
- package/dist/ui/summary.d.ts +18 -0
- package/dist/ui/summary.js +113 -0
- package/dist/ui/theme.d.ts +63 -0
- package/dist/ui/theme.js +97 -0
- package/dist/viewer.d.ts +12 -0
- package/dist/viewer.js +1284 -0
- package/dist/worktree/manager.d.ts +45 -0
- package/dist/worktree/manager.js +266 -0
- package/dist/worktree/merge.d.ts +28 -0
- package/dist/worktree/merge.js +138 -0
- package/package.json +69 -0
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* swarm — Swarm-native coding agent orchestrator
|
|
4
|
+
*
|
|
5
|
+
* Entry point for the `swarm` command.
|
|
6
|
+
*
|
|
7
|
+
* swarm --dir ./project "task" → swarm mode (coding agent orchestration)
|
|
8
|
+
* swarm --dir ./project → interactive REPL (no query)
|
|
9
|
+
* swarm mcp [--dir ./project] → MCP server (stdio transport)
|
|
10
|
+
* swarm run → single-shot RLM CLI run
|
|
11
|
+
* swarm viewer → browse trajectory files
|
|
12
|
+
* swarm benchmark → run benchmarks
|
|
13
|
+
* swarm → interactive terminal (RLM mode, default)
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildHelp(): string;
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* swarm — Swarm-native coding agent orchestrator
|
|
4
|
+
*
|
|
5
|
+
* Entry point for the `swarm` command.
|
|
6
|
+
*
|
|
7
|
+
* swarm --dir ./project "task" → swarm mode (coding agent orchestration)
|
|
8
|
+
* swarm --dir ./project → interactive REPL (no query)
|
|
9
|
+
* swarm mcp [--dir ./project] → MCP server (stdio transport)
|
|
10
|
+
* swarm run → single-shot RLM CLI run
|
|
11
|
+
* swarm viewer → browse trajectory files
|
|
12
|
+
* swarm benchmark → run benchmarks
|
|
13
|
+
* swarm → interactive terminal (RLM mode, default)
|
|
14
|
+
*/
|
|
15
|
+
import { bold, coral, cyan, dim, isTTY, symbols, termWidth, yellow } from "./ui/theme.js";
|
|
16
|
+
export function buildHelp() {
|
|
17
|
+
const w = Math.max(Math.min(termWidth(), 60), 24);
|
|
18
|
+
const lines = [];
|
|
19
|
+
if (isTTY) {
|
|
20
|
+
const title = " swarm ";
|
|
21
|
+
const sub = " cli ";
|
|
22
|
+
const padLen = Math.max(0, w - title.length - sub.length - 4);
|
|
23
|
+
const l = symbols.horizontal.repeat(Math.floor(padLen / 2));
|
|
24
|
+
const r = symbols.horizontal.repeat(Math.ceil(padLen / 2));
|
|
25
|
+
lines.push("");
|
|
26
|
+
lines.push(` ${cyan(`${symbols.topLeft}${l}`)}${bold(coral(title))}${dim(sub)}${cyan(`${r}${symbols.topRight}`)}`);
|
|
27
|
+
lines.push(` ${cyan(symbols.vertLine)}${" ".repeat(w - 2)}${cyan(symbols.vertLine)}`);
|
|
28
|
+
lines.push(` ${cyan(symbols.vertLine)} ${dim("Open-source orchestrator for parallel coding agents")}${" ".repeat(Math.max(0, w - 55))}${cyan(symbols.vertLine)}`);
|
|
29
|
+
lines.push(` ${cyan(symbols.vertLine)} ${dim("Built on RLM (arXiv:2512.24601)")}${" ".repeat(Math.max(0, w - 36))}${cyan(symbols.vertLine)}`);
|
|
30
|
+
lines.push(` ${cyan(symbols.vertLine)}${" ".repeat(w - 2)}${cyan(symbols.vertLine)}`);
|
|
31
|
+
lines.push(` ${cyan(symbols.bottomLeft)}${cyan(symbols.horizontal.repeat(w - 2))}${cyan(symbols.bottomRight)}`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
lines.push("\nswarm — Open-source orchestrator for parallel coding agents");
|
|
35
|
+
}
|
|
36
|
+
lines.push("");
|
|
37
|
+
lines.push(` ${bold("SWARM MODE")} ${dim("(coding agent orchestration)")}`);
|
|
38
|
+
lines.push(` ${yellow("swarm")} --dir ./project ${dim('"add error handling to all API routes"')}`);
|
|
39
|
+
lines.push(` ${yellow("swarm")} --dir ./project --orchestrator claude-sonnet-4-6 ${dim('"task"')}`);
|
|
40
|
+
lines.push(` ${yellow("swarm")} --dir ./project --dry-run ${dim('"plan refactor"')}`);
|
|
41
|
+
lines.push(` ${yellow("swarm")} --dir ./project --max-budget 5.00 ${dim('"task"')}`);
|
|
42
|
+
lines.push(` ${yellow("swarm")} --dir ./project ${dim("Interactive REPL (no query)")}`);
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push(` ${bold("MCP SERVER")} ${dim("(expose swarm as tools for Claude Code, Cursor, etc.)")}`);
|
|
45
|
+
lines.push(` ${yellow("swarm mcp")} ${dim("Start MCP server (stdio)")}`);
|
|
46
|
+
lines.push(` ${yellow("swarm mcp")} --dir ./project ${dim("Start with default directory")}`);
|
|
47
|
+
lines.push("");
|
|
48
|
+
lines.push(` ${bold("RLM MODE")} ${dim("(text processing, inherited from rlm-cli)")}`);
|
|
49
|
+
lines.push(` ${yellow("swarm")} ${dim("Interactive terminal (default)")}`);
|
|
50
|
+
lines.push(` ${yellow("swarm run")} [options] "<query>" ${dim("Run a single query")}`);
|
|
51
|
+
lines.push(` ${yellow("swarm viewer")} ${dim("Browse saved trajectory files")}`);
|
|
52
|
+
lines.push(` ${yellow("swarm benchmark")} <name> [--idx] ${dim("Run benchmark")}`);
|
|
53
|
+
lines.push("");
|
|
54
|
+
lines.push(` ${bold("SWARM OPTIONS")}`);
|
|
55
|
+
lines.push(` ${cyan("--dir")} <path> Target repository directory`);
|
|
56
|
+
lines.push(` ${cyan("--orchestrator")} <model> Model for the orchestrator LLM`);
|
|
57
|
+
lines.push(` ${cyan("--agent")} <backend> Default agent backend ${dim("(opencode)")}`);
|
|
58
|
+
lines.push(` ${cyan("--dry-run")} Plan only, don't spawn threads`);
|
|
59
|
+
lines.push(` ${cyan("--max-budget")} <usd> Maximum session budget in USD`);
|
|
60
|
+
lines.push(` ${cyan("--verbose")} Show detailed progress`);
|
|
61
|
+
lines.push(` ${cyan("--quiet")} / ${cyan("-q")} Suppress non-essential output`);
|
|
62
|
+
lines.push(` ${cyan("--json")} Machine-readable JSON output`);
|
|
63
|
+
lines.push("");
|
|
64
|
+
lines.push(` ${bold("RUN OPTIONS")}`);
|
|
65
|
+
lines.push(` ${cyan("--model")} <id> Override model ${dim("(RLM_MODEL from .env)")}`);
|
|
66
|
+
lines.push(` ${cyan("--file")} <path> Read context from a file`);
|
|
67
|
+
lines.push(` ${cyan("--url")} <url> Fetch context from a URL`);
|
|
68
|
+
lines.push(` ${cyan("--stdin")} Read context from stdin`);
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push(` ${bold("CONFIGURATION")}`);
|
|
71
|
+
lines.push(` ${dim(".env file (pick one provider):")}`);
|
|
72
|
+
lines.push(` ANTHROPIC_API_KEY=sk-ant-...`);
|
|
73
|
+
lines.push(` OPENAI_API_KEY=sk-...`);
|
|
74
|
+
lines.push(` GEMINI_API_KEY=AIza...`);
|
|
75
|
+
lines.push("");
|
|
76
|
+
lines.push(` ${dim("swarm_config.yaml:")}`);
|
|
77
|
+
lines.push(` max_threads: 5`);
|
|
78
|
+
lines.push(` default_agent: opencode`);
|
|
79
|
+
lines.push(` compression_strategy: structured`);
|
|
80
|
+
return lines.join("\n");
|
|
81
|
+
}
|
|
82
|
+
// Lazy — evaluated on first use, not at module load
|
|
83
|
+
let _help;
|
|
84
|
+
function getHelp() {
|
|
85
|
+
if (!_help)
|
|
86
|
+
_help = buildHelp();
|
|
87
|
+
return _help;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check if args contain positional (non-flag) arguments.
|
|
91
|
+
* Skips known flags that take a value argument.
|
|
92
|
+
*/
|
|
93
|
+
function hasPositionalArgs(args) {
|
|
94
|
+
const flagsWithValue = new Set(["--dir", "--orchestrator", "--agent", "--max-budget", "--model", "--file", "--url"]);
|
|
95
|
+
for (let i = 0; i < args.length; i++) {
|
|
96
|
+
const arg = args[i];
|
|
97
|
+
if (arg.startsWith("--") || arg === "-q" || arg === "-h") {
|
|
98
|
+
// Skip flags with values
|
|
99
|
+
if (flagsWithValue.has(arg) && i + 1 < args.length) {
|
|
100
|
+
i++; // skip the value
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// Found a positional argument
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
async function main() {
|
|
110
|
+
const args = process.argv.slice(2);
|
|
111
|
+
// Check if this is swarm mode (has --dir flag)
|
|
112
|
+
const dirIdx = args.indexOf("--dir");
|
|
113
|
+
if (dirIdx !== -1) {
|
|
114
|
+
// Check if there's a query (positional args that aren't flags/flag-values)
|
|
115
|
+
const hasQuery = hasPositionalArgs(args);
|
|
116
|
+
if (hasQuery) {
|
|
117
|
+
// Single-shot swarm mode — dynamic import to avoid loading all swarm deps upfront
|
|
118
|
+
const { runSwarmMode } = await import("./swarm.js");
|
|
119
|
+
await runSwarmMode(args);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
// Interactive swarm mode — no query provided, launch REPL
|
|
123
|
+
const { runInteractiveSwarm } = await import("./interactive-swarm.js");
|
|
124
|
+
await runInteractiveSwarm(args);
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const command = args[0] || "interactive";
|
|
129
|
+
switch (command) {
|
|
130
|
+
case "interactive":
|
|
131
|
+
case "i": {
|
|
132
|
+
await import("./interactive.js");
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case "viewer":
|
|
136
|
+
case "view": {
|
|
137
|
+
process.argv = [process.argv[0], process.argv[1], ...args.slice(1)];
|
|
138
|
+
await import("./viewer.js");
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case "mcp": {
|
|
142
|
+
const { startMcpServer } = await import("./mcp/server.js");
|
|
143
|
+
await startMcpServer(args.slice(1));
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
case "run": {
|
|
147
|
+
process.argv = [process.argv[0], process.argv[1], ...args.slice(1)];
|
|
148
|
+
await import("./cli.js");
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
case "benchmark":
|
|
152
|
+
case "bench": {
|
|
153
|
+
const benchName = args[1];
|
|
154
|
+
const benchArgs = args.slice(2);
|
|
155
|
+
const benchScripts = {
|
|
156
|
+
oolong: "benchmarks/oolong_synth.ts",
|
|
157
|
+
longbench: "benchmarks/longbench_narrativeqa.ts",
|
|
158
|
+
};
|
|
159
|
+
if (benchName && benchScripts[benchName]) {
|
|
160
|
+
const { spawn } = await import("node:child_process");
|
|
161
|
+
const { dirname, join } = await import("node:path");
|
|
162
|
+
const { fileURLToPath } = await import("node:url");
|
|
163
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
164
|
+
const script = join(root, benchScripts[benchName]);
|
|
165
|
+
const tsxBin = join(root, "node_modules", ".bin", "tsx");
|
|
166
|
+
await new Promise((resolve, reject) => {
|
|
167
|
+
const child = spawn(tsxBin, [script, ...benchArgs], {
|
|
168
|
+
stdio: "inherit",
|
|
169
|
+
cwd: root,
|
|
170
|
+
});
|
|
171
|
+
child.on("exit", (code) => {
|
|
172
|
+
process.exitCode = code ?? 1;
|
|
173
|
+
resolve();
|
|
174
|
+
});
|
|
175
|
+
child.on("error", (err) => {
|
|
176
|
+
reject(new Error(`Failed to spawn benchmark: ${err.message}`));
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
console.log(`${cyan(bold("swarm benchmark"))} ${dim("— Run direct LLM vs RLM comparison")}\n`);
|
|
182
|
+
console.log(bold("USAGE"));
|
|
183
|
+
console.log(` ${yellow("swarm benchmark oolong")} [--idx N] Oolong Synth`);
|
|
184
|
+
console.log(` ${yellow("swarm benchmark longbench")} [--idx N] LongBench NarrativeQA\n`);
|
|
185
|
+
}
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case "help":
|
|
189
|
+
case "--help":
|
|
190
|
+
case "-h": {
|
|
191
|
+
console.log(getHelp());
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case "version":
|
|
195
|
+
case "--version":
|
|
196
|
+
case "-v": {
|
|
197
|
+
try {
|
|
198
|
+
const { readFileSync } = await import("node:fs");
|
|
199
|
+
const { dirname, join } = await import("node:path");
|
|
200
|
+
const { fileURLToPath } = await import("node:url");
|
|
201
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
202
|
+
const pkgPath = join(__dir, "..", "package.json");
|
|
203
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
204
|
+
console.log(`swarm v${pkg.version}`);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
console.log("swarm (version unknown)");
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
default: {
|
|
212
|
+
if (command.startsWith("--")) {
|
|
213
|
+
// Flags without subcommand — check for --dir (swarm mode)
|
|
214
|
+
if (command === "--dir") {
|
|
215
|
+
if (hasPositionalArgs(args)) {
|
|
216
|
+
const { runSwarmMode } = await import("./swarm.js");
|
|
217
|
+
await runSwarmMode(args);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const { runInteractiveSwarm } = await import("./interactive-swarm.js");
|
|
221
|
+
await runInteractiveSwarm(args);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
// Assume "run" mode, pass all args through
|
|
226
|
+
process.argv = [process.argv[0], process.argv[1], ...args];
|
|
227
|
+
await import("./cli.js");
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
console.error(`Unknown command: ${command}`);
|
|
232
|
+
console.error('Run "swarm help" for usage information.');
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
main().catch((err) => {
|
|
239
|
+
console.error("Fatal error:", err);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
});
|
|
242
|
+
//# sourceMappingURL=main.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server for swarm-code.
|
|
3
|
+
*
|
|
4
|
+
* Exposes swarm capabilities as MCP tools that can be called by
|
|
5
|
+
* Claude Code, Cursor, or any MCP-compatible client.
|
|
6
|
+
*
|
|
7
|
+
* Transport: stdio (reads JSON-RPC from stdin, writes to stdout).
|
|
8
|
+
* IMPORTANT: Never use console.log() — it corrupts the MCP protocol.
|
|
9
|
+
* All logging goes to stderr via process.stderr.write().
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* swarm mcp # Start MCP server
|
|
13
|
+
* swarm mcp --dir ./my-project # Start with default directory
|
|
14
|
+
*/
|
|
15
|
+
export declare function startMcpServer(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP server for swarm-code.
|
|
3
|
+
*
|
|
4
|
+
* Exposes swarm capabilities as MCP tools that can be called by
|
|
5
|
+
* Claude Code, Cursor, or any MCP-compatible client.
|
|
6
|
+
*
|
|
7
|
+
* Transport: stdio (reads JSON-RPC from stdin, writes to stdout).
|
|
8
|
+
* IMPORTANT: Never use console.log() — it corrupts the MCP protocol.
|
|
9
|
+
* All logging goes to stderr via process.stderr.write().
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* swarm mcp # Start MCP server
|
|
13
|
+
* swarm mcp --dir ./my-project # Start with default directory
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { dirname, join } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
+
import { cleanupAllSessions } from "./session.js";
|
|
21
|
+
import { killActiveSubprocesses, registerTools } from "./tools.js";
|
|
22
|
+
// ── Logging ────────────────────────────────────────────────────────────────
|
|
23
|
+
function log(msg) {
|
|
24
|
+
process.stderr.write(`[swarm-mcp] ${msg}\n`);
|
|
25
|
+
}
|
|
26
|
+
// ── Server ─────────────────────────────────────────────────────────────────
|
|
27
|
+
export async function startMcpServer(args) {
|
|
28
|
+
// Parse --dir from args
|
|
29
|
+
let defaultDir;
|
|
30
|
+
const dirIdx = args.indexOf("--dir");
|
|
31
|
+
if (dirIdx !== -1 && dirIdx + 1 < args.length) {
|
|
32
|
+
defaultDir = args[dirIdx + 1];
|
|
33
|
+
}
|
|
34
|
+
// Create MCP server
|
|
35
|
+
const server = new McpServer({
|
|
36
|
+
name: "swarm-code",
|
|
37
|
+
version: getVersion(),
|
|
38
|
+
}, {
|
|
39
|
+
capabilities: {
|
|
40
|
+
tools: {},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
// Register all tools
|
|
44
|
+
registerTools(server, defaultDir);
|
|
45
|
+
// Handle graceful shutdown — kill subprocesses, cleanup sessions
|
|
46
|
+
const shutdown = async () => {
|
|
47
|
+
log("Shutting down...");
|
|
48
|
+
killActiveSubprocesses();
|
|
49
|
+
await cleanupAllSessions();
|
|
50
|
+
await server.close();
|
|
51
|
+
process.exit(0);
|
|
52
|
+
};
|
|
53
|
+
process.on("SIGINT", shutdown);
|
|
54
|
+
process.on("SIGTERM", shutdown);
|
|
55
|
+
// Connect via stdio transport
|
|
56
|
+
const transport = new StdioServerTransport();
|
|
57
|
+
await server.connect(transport);
|
|
58
|
+
log(`Server started${defaultDir ? ` (default dir: ${defaultDir})` : ""}`);
|
|
59
|
+
}
|
|
60
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
61
|
+
function getVersion() {
|
|
62
|
+
try {
|
|
63
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
64
|
+
const pkgPath = join(__dir, "..", "..", "package.json");
|
|
65
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
66
|
+
return pkg.version || "0.1.0";
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return "0.1.0";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP session manager — maintains per-directory swarm state.
|
|
3
|
+
*
|
|
4
|
+
* Each directory gets its own session with:
|
|
5
|
+
* - ThreadManager (spawning/tracking threads)
|
|
6
|
+
* - SwarmConfig (loaded from the project dir)
|
|
7
|
+
* - AbortController (for cancellation)
|
|
8
|
+
* - BudgetState (cost tracking)
|
|
9
|
+
*
|
|
10
|
+
* Sessions are lazily initialized on first tool call and persist
|
|
11
|
+
* across multiple MCP tool invocations.
|
|
12
|
+
*
|
|
13
|
+
* Concurrency:
|
|
14
|
+
* - pendingSessions deduplicates concurrent init for the same dir
|
|
15
|
+
* - loadConfig(cwd) avoids process.chdir() race conditions
|
|
16
|
+
*/
|
|
17
|
+
import type { SwarmConfig } from "../config.js";
|
|
18
|
+
import type { BudgetState, CompressedResult, MergeResult, ThreadState } from "../core/types.js";
|
|
19
|
+
import { ThreadManager } from "../threads/manager.js";
|
|
20
|
+
export interface SwarmSession {
|
|
21
|
+
dir: string;
|
|
22
|
+
config: SwarmConfig;
|
|
23
|
+
threadManager: ThreadManager;
|
|
24
|
+
abortController: AbortController;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
}
|
|
27
|
+
export interface ThreadSpawnParams {
|
|
28
|
+
task: string;
|
|
29
|
+
files?: string[];
|
|
30
|
+
agent?: string;
|
|
31
|
+
model?: string;
|
|
32
|
+
context?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Get or create a session for a directory.
|
|
36
|
+
* The directory is resolved to an absolute path and used as the session key.
|
|
37
|
+
* Concurrent calls for the same directory are deduplicated.
|
|
38
|
+
*/
|
|
39
|
+
export declare function getSession(dir: string): Promise<SwarmSession>;
|
|
40
|
+
/**
|
|
41
|
+
* Spawn a thread in a session.
|
|
42
|
+
*/
|
|
43
|
+
export declare function spawnThread(session: SwarmSession, params: ThreadSpawnParams): Promise<CompressedResult>;
|
|
44
|
+
/**
|
|
45
|
+
* Get all threads in a session.
|
|
46
|
+
*/
|
|
47
|
+
export declare function getThreads(session: SwarmSession): ThreadState[];
|
|
48
|
+
/**
|
|
49
|
+
* Get budget state for a session.
|
|
50
|
+
*/
|
|
51
|
+
export declare function getBudgetState(session: SwarmSession): BudgetState;
|
|
52
|
+
/**
|
|
53
|
+
* Merge completed threads.
|
|
54
|
+
*/
|
|
55
|
+
export declare function mergeThreads(session: SwarmSession): Promise<MergeResult[]>;
|
|
56
|
+
/**
|
|
57
|
+
* Cancel a specific thread or all threads.
|
|
58
|
+
* Per-thread cancellation uses ThreadManager.cancelThread() which
|
|
59
|
+
* aborts the thread's individual AbortController.
|
|
60
|
+
*/
|
|
61
|
+
export declare function cancelThreads(session: SwarmSession, threadId?: string): {
|
|
62
|
+
cancelled: boolean;
|
|
63
|
+
message: string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Cleanup a session — destroy worktrees, remove session.
|
|
67
|
+
*/
|
|
68
|
+
export declare function cleanupSession(dir: string): Promise<string>;
|
|
69
|
+
/**
|
|
70
|
+
* Cleanup all sessions. Snapshots keys first to avoid
|
|
71
|
+
* mutating the map during iteration.
|
|
72
|
+
*/
|
|
73
|
+
export declare function cleanupAllSessions(): Promise<void>;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP session manager — maintains per-directory swarm state.
|
|
3
|
+
*
|
|
4
|
+
* Each directory gets its own session with:
|
|
5
|
+
* - ThreadManager (spawning/tracking threads)
|
|
6
|
+
* - SwarmConfig (loaded from the project dir)
|
|
7
|
+
* - AbortController (for cancellation)
|
|
8
|
+
* - BudgetState (cost tracking)
|
|
9
|
+
*
|
|
10
|
+
* Sessions are lazily initialized on first tool call and persist
|
|
11
|
+
* across multiple MCP tool invocations.
|
|
12
|
+
*
|
|
13
|
+
* Concurrency:
|
|
14
|
+
* - pendingSessions deduplicates concurrent init for the same dir
|
|
15
|
+
* - loadConfig(cwd) avoids process.chdir() race conditions
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import { loadConfig } from "../config.js";
|
|
20
|
+
import { ThreadManager } from "../threads/manager.js";
|
|
21
|
+
import { mergeAllThreads } from "../worktree/merge.js";
|
|
22
|
+
// ── Session Manager ────────────────────────────────────────────────────────
|
|
23
|
+
const sessions = new Map();
|
|
24
|
+
/** Deduplicates concurrent getSession() calls for the same directory. */
|
|
25
|
+
const pendingSessions = new Map();
|
|
26
|
+
/**
|
|
27
|
+
* Lazily init agent backends (only once).
|
|
28
|
+
* Agent modules self-register when imported.
|
|
29
|
+
* The flag is set eagerly to prevent duplicate imports even if
|
|
30
|
+
* the first call hasn't finished awaiting yet.
|
|
31
|
+
*/
|
|
32
|
+
let agentsRegistered = false;
|
|
33
|
+
async function ensureAgentsRegistered() {
|
|
34
|
+
if (agentsRegistered)
|
|
35
|
+
return;
|
|
36
|
+
agentsRegistered = true;
|
|
37
|
+
// Each agent module calls registerAgent() at module level on import
|
|
38
|
+
const modules = [
|
|
39
|
+
import("../agents/opencode.js"),
|
|
40
|
+
import("../agents/claude-code.js"),
|
|
41
|
+
import("../agents/codex.js"),
|
|
42
|
+
import("../agents/aider.js"),
|
|
43
|
+
import("../agents/direct-llm.js"),
|
|
44
|
+
];
|
|
45
|
+
// Import all, ignoring individual failures
|
|
46
|
+
await Promise.allSettled(modules);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Get or create a session for a directory.
|
|
50
|
+
* The directory is resolved to an absolute path and used as the session key.
|
|
51
|
+
* Concurrent calls for the same directory are deduplicated.
|
|
52
|
+
*/
|
|
53
|
+
export async function getSession(dir) {
|
|
54
|
+
const absDir = path.resolve(dir);
|
|
55
|
+
if (!fs.existsSync(absDir)) {
|
|
56
|
+
throw new Error(`Directory does not exist: ${absDir}`);
|
|
57
|
+
}
|
|
58
|
+
// Return existing session
|
|
59
|
+
const existing = sessions.get(absDir);
|
|
60
|
+
if (existing)
|
|
61
|
+
return existing;
|
|
62
|
+
// Deduplicate concurrent init for the same dir
|
|
63
|
+
const pending = pendingSessions.get(absDir);
|
|
64
|
+
if (pending)
|
|
65
|
+
return pending;
|
|
66
|
+
const initPromise = initSession(absDir);
|
|
67
|
+
pendingSessions.set(absDir, initPromise);
|
|
68
|
+
try {
|
|
69
|
+
const session = await initPromise;
|
|
70
|
+
return session;
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
pendingSessions.delete(absDir);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Initialize a new session for a directory.
|
|
78
|
+
* Uses loadConfig(cwd) to avoid process.chdir() race conditions.
|
|
79
|
+
*/
|
|
80
|
+
async function initSession(absDir) {
|
|
81
|
+
await ensureAgentsRegistered();
|
|
82
|
+
// Load config from project dir without chdir (concurrency-safe)
|
|
83
|
+
const config = loadConfig(absDir);
|
|
84
|
+
const abortController = new AbortController();
|
|
85
|
+
// ThreadManager creates its own WorktreeManager internally,
|
|
86
|
+
// so we don't need a separate one at the session level.
|
|
87
|
+
const threadManager = new ThreadManager(absDir, config,
|
|
88
|
+
// Progress callback — log to stderr (stdout is MCP protocol)
|
|
89
|
+
(threadId, phase, detail) => {
|
|
90
|
+
const msg = detail ? `[${threadId}] ${phase}: ${detail}` : `[${threadId}] ${phase}`;
|
|
91
|
+
process.stderr.write(`[swarm-mcp] ${msg}\n`);
|
|
92
|
+
}, abortController.signal);
|
|
93
|
+
await threadManager.init();
|
|
94
|
+
const session = {
|
|
95
|
+
dir: absDir,
|
|
96
|
+
config,
|
|
97
|
+
threadManager,
|
|
98
|
+
abortController,
|
|
99
|
+
createdAt: Date.now(),
|
|
100
|
+
};
|
|
101
|
+
sessions.set(absDir, session);
|
|
102
|
+
return session;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Spawn a thread in a session.
|
|
106
|
+
*/
|
|
107
|
+
export async function spawnThread(session, params) {
|
|
108
|
+
const threadId = `mcp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
109
|
+
const threadConfig = {
|
|
110
|
+
id: threadId,
|
|
111
|
+
task: params.task,
|
|
112
|
+
context: params.context || "",
|
|
113
|
+
agent: {
|
|
114
|
+
backend: params.agent || session.config.default_agent,
|
|
115
|
+
model: params.model || session.config.default_model,
|
|
116
|
+
},
|
|
117
|
+
files: params.files || [],
|
|
118
|
+
};
|
|
119
|
+
return session.threadManager.spawnThread(threadConfig);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get all threads in a session.
|
|
123
|
+
*/
|
|
124
|
+
export function getThreads(session) {
|
|
125
|
+
return session.threadManager.getThreads();
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Get budget state for a session.
|
|
129
|
+
*/
|
|
130
|
+
export function getBudgetState(session) {
|
|
131
|
+
return session.threadManager.getBudgetState();
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Merge completed threads.
|
|
135
|
+
*/
|
|
136
|
+
export async function mergeThreads(session) {
|
|
137
|
+
const threads = session.threadManager.getThreads();
|
|
138
|
+
return mergeAllThreads(session.dir, threads, { continueOnConflict: true });
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Cancel a specific thread or all threads.
|
|
142
|
+
* Per-thread cancellation uses ThreadManager.cancelThread() which
|
|
143
|
+
* aborts the thread's individual AbortController.
|
|
144
|
+
*/
|
|
145
|
+
export function cancelThreads(session, threadId) {
|
|
146
|
+
if (threadId) {
|
|
147
|
+
const cancelled = session.threadManager.cancelThread(threadId);
|
|
148
|
+
if (!cancelled) {
|
|
149
|
+
const threads = session.threadManager.getThreads();
|
|
150
|
+
const thread = threads.find((t) => t.id === threadId);
|
|
151
|
+
if (!thread)
|
|
152
|
+
return { cancelled: false, message: `Thread ${threadId} not found` };
|
|
153
|
+
return { cancelled: false, message: `Thread ${threadId} is ${thread.status}, cannot cancel` };
|
|
154
|
+
}
|
|
155
|
+
return { cancelled: true, message: `Thread ${threadId} cancelled` };
|
|
156
|
+
}
|
|
157
|
+
// Cancel all — abort the session controller
|
|
158
|
+
session.abortController.abort();
|
|
159
|
+
return { cancelled: true, message: "All threads cancelled" };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Cleanup a session — destroy worktrees, remove session.
|
|
163
|
+
*/
|
|
164
|
+
export async function cleanupSession(dir) {
|
|
165
|
+
const absDir = path.resolve(dir);
|
|
166
|
+
const session = sessions.get(absDir);
|
|
167
|
+
if (!session)
|
|
168
|
+
return "No active session for this directory";
|
|
169
|
+
session.abortController.abort();
|
|
170
|
+
await session.threadManager.cleanup();
|
|
171
|
+
sessions.delete(absDir);
|
|
172
|
+
return `Session cleaned up for ${absDir}`;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Cleanup all sessions. Snapshots keys first to avoid
|
|
176
|
+
* mutating the map during iteration.
|
|
177
|
+
*/
|
|
178
|
+
export async function cleanupAllSessions() {
|
|
179
|
+
const dirs = [...sessions.keys()];
|
|
180
|
+
for (const dir of dirs) {
|
|
181
|
+
await cleanupSession(dir);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
//# sourceMappingURL=session.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool definitions and handlers for swarm-code.
|
|
3
|
+
*
|
|
4
|
+
* Tools:
|
|
5
|
+
* - swarm_run: Full orchestrated swarm execution (subprocess)
|
|
6
|
+
* - swarm_thread: Spawn a single coding agent thread in a worktree
|
|
7
|
+
* - swarm_status: Get current session status (threads, budget)
|
|
8
|
+
* - swarm_merge: Merge completed thread branches
|
|
9
|
+
* - swarm_cancel: Cancel running thread(s)
|
|
10
|
+
* - swarm_cleanup: Destroy session and worktrees
|
|
11
|
+
*/
|
|
12
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
13
|
+
/** Kill all tracked subprocesses. Called during server shutdown. */
|
|
14
|
+
export declare function killActiveSubprocesses(): void;
|
|
15
|
+
export declare function registerTools(server: McpServer, defaultDir?: string): void;
|