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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +384 -0
  3. package/bin/swarm.mjs +45 -0
  4. package/dist/agents/aider.d.ts +12 -0
  5. package/dist/agents/aider.js +182 -0
  6. package/dist/agents/claude-code.d.ts +9 -0
  7. package/dist/agents/claude-code.js +216 -0
  8. package/dist/agents/codex.d.ts +14 -0
  9. package/dist/agents/codex.js +193 -0
  10. package/dist/agents/direct-llm.d.ts +9 -0
  11. package/dist/agents/direct-llm.js +78 -0
  12. package/dist/agents/mock.d.ts +9 -0
  13. package/dist/agents/mock.js +77 -0
  14. package/dist/agents/opencode.d.ts +23 -0
  15. package/dist/agents/opencode.js +571 -0
  16. package/dist/agents/provider.d.ts +11 -0
  17. package/dist/agents/provider.js +31 -0
  18. package/dist/cli.d.ts +15 -0
  19. package/dist/cli.js +285 -0
  20. package/dist/compression/compressor.d.ts +28 -0
  21. package/dist/compression/compressor.js +265 -0
  22. package/dist/config.d.ts +42 -0
  23. package/dist/config.js +170 -0
  24. package/dist/core/repl.d.ts +69 -0
  25. package/dist/core/repl.js +336 -0
  26. package/dist/core/rlm.d.ts +63 -0
  27. package/dist/core/rlm.js +409 -0
  28. package/dist/core/runtime.py +335 -0
  29. package/dist/core/types.d.ts +131 -0
  30. package/dist/core/types.js +19 -0
  31. package/dist/env.d.ts +10 -0
  32. package/dist/env.js +75 -0
  33. package/dist/interactive-swarm.d.ts +20 -0
  34. package/dist/interactive-swarm.js +1041 -0
  35. package/dist/interactive.d.ts +10 -0
  36. package/dist/interactive.js +1765 -0
  37. package/dist/main.d.ts +15 -0
  38. package/dist/main.js +242 -0
  39. package/dist/mcp/server.d.ts +15 -0
  40. package/dist/mcp/server.js +72 -0
  41. package/dist/mcp/session.d.ts +73 -0
  42. package/dist/mcp/session.js +184 -0
  43. package/dist/mcp/tools.d.ts +15 -0
  44. package/dist/mcp/tools.js +377 -0
  45. package/dist/memory/episodic.d.ts +132 -0
  46. package/dist/memory/episodic.js +390 -0
  47. package/dist/prompts/orchestrator.d.ts +5 -0
  48. package/dist/prompts/orchestrator.js +191 -0
  49. package/dist/routing/model-router.d.ts +130 -0
  50. package/dist/routing/model-router.js +515 -0
  51. package/dist/swarm.d.ts +14 -0
  52. package/dist/swarm.js +557 -0
  53. package/dist/threads/cache.d.ts +58 -0
  54. package/dist/threads/cache.js +198 -0
  55. package/dist/threads/manager.d.ts +85 -0
  56. package/dist/threads/manager.js +659 -0
  57. package/dist/ui/banner.d.ts +14 -0
  58. package/dist/ui/banner.js +42 -0
  59. package/dist/ui/dashboard.d.ts +33 -0
  60. package/dist/ui/dashboard.js +151 -0
  61. package/dist/ui/index.d.ts +10 -0
  62. package/dist/ui/index.js +11 -0
  63. package/dist/ui/log.d.ts +39 -0
  64. package/dist/ui/log.js +126 -0
  65. package/dist/ui/onboarding.d.ts +14 -0
  66. package/dist/ui/onboarding.js +518 -0
  67. package/dist/ui/spinner.d.ts +25 -0
  68. package/dist/ui/spinner.js +113 -0
  69. package/dist/ui/summary.d.ts +18 -0
  70. package/dist/ui/summary.js +113 -0
  71. package/dist/ui/theme.d.ts +63 -0
  72. package/dist/ui/theme.js +97 -0
  73. package/dist/viewer.d.ts +12 -0
  74. package/dist/viewer.js +1284 -0
  75. package/dist/worktree/manager.d.ts +45 -0
  76. package/dist/worktree/manager.js +266 -0
  77. package/dist/worktree/merge.d.ts +28 -0
  78. package/dist/worktree/merge.js +138 -0
  79. 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;