multiagents 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/.mcp.json +12 -0
- package/README.md +184 -0
- package/adapters/base-adapter.ts +1493 -0
- package/adapters/claude-adapter.ts +66 -0
- package/adapters/codex-adapter.ts +135 -0
- package/adapters/gemini-adapter.ts +129 -0
- package/broker.ts +1263 -0
- package/cli/commands.ts +194 -0
- package/cli/dashboard.ts +988 -0
- package/cli/session.ts +278 -0
- package/cli/setup.ts +257 -0
- package/cli.ts +17 -0
- package/index.ts +41 -0
- package/noop-mcp.ts +63 -0
- package/orchestrator/guardrails.ts +243 -0
- package/orchestrator/launcher.ts +433 -0
- package/orchestrator/monitor.ts +285 -0
- package/orchestrator/orchestrator-server.ts +1000 -0
- package/orchestrator/progress.ts +214 -0
- package/orchestrator/recovery.ts +176 -0
- package/orchestrator/session-control.ts +343 -0
- package/package.json +70 -0
- package/scripts/postinstall.ts +84 -0
- package/scripts/version.ts +62 -0
- package/server.ts +52 -0
- package/shared/broker-client.ts +243 -0
- package/shared/constants.ts +148 -0
- package/shared/summarize.ts +97 -0
- package/shared/types.ts +419 -0
- package/shared/utils.ts +121 -0
- package/tsconfig.json +29 -0
package/cli/session.ts
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// multiagents — Session Management Commands
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { DEFAULT_BROKER_PORT, BROKER_HOSTNAME, SESSION_DIR, SESSION_FILE } from "../shared/constants.ts";
|
|
6
|
+
import { BrokerClient } from "../shared/broker-client.ts";
|
|
7
|
+
import { getGitRoot, formatTime, timeSince, slugify } from "../shared/utils.ts";
|
|
8
|
+
import type { SessionFile } from "../shared/types.ts";
|
|
9
|
+
import * as readline from "node:readline";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
|
|
13
|
+
const BROKER_PORT = parseInt(process.env.MULTIAGENTS_PORT ?? String(DEFAULT_BROKER_PORT), 10);
|
|
14
|
+
const BROKER_URL = `http://${BROKER_HOSTNAME}:${BROKER_PORT}`;
|
|
15
|
+
|
|
16
|
+
function prompt(question: string): Promise<string> {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
19
|
+
rl.question(`${question}: `, (answer) => {
|
|
20
|
+
rl.close();
|
|
21
|
+
resolve(answer.trim());
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readLocalSession(): SessionFile | null {
|
|
27
|
+
try {
|
|
28
|
+
const text = fs.readFileSync(path.resolve(process.cwd(), SESSION_FILE), "utf-8");
|
|
29
|
+
return JSON.parse(text) as SessionFile;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function sessionCommand(args: string[]): Promise<void> {
|
|
36
|
+
const sub = args[0];
|
|
37
|
+
const client = new BrokerClient(BROKER_URL);
|
|
38
|
+
|
|
39
|
+
switch (sub) {
|
|
40
|
+
case "create":
|
|
41
|
+
await create(client, args.slice(1).join(" ") || undefined);
|
|
42
|
+
break;
|
|
43
|
+
case "list":
|
|
44
|
+
await list(client);
|
|
45
|
+
break;
|
|
46
|
+
case "resume":
|
|
47
|
+
await resume(client, args[1]);
|
|
48
|
+
break;
|
|
49
|
+
case "pause":
|
|
50
|
+
await pause(client, args[1]);
|
|
51
|
+
break;
|
|
52
|
+
case "archive":
|
|
53
|
+
await archive(client, args[1]);
|
|
54
|
+
break;
|
|
55
|
+
case "delete":
|
|
56
|
+
await deleteSession(client, args[1]);
|
|
57
|
+
break;
|
|
58
|
+
case "export":
|
|
59
|
+
await exportSession(client, args[1]);
|
|
60
|
+
break;
|
|
61
|
+
default:
|
|
62
|
+
console.log(`Usage: multiagents session <create|list|resume|pause|archive|delete|export> [args]`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function create(client: BrokerClient, name?: string): Promise<void> {
|
|
68
|
+
if (!name) {
|
|
69
|
+
console.error("Usage: multiagents session create <name>");
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const projectDir = process.cwd();
|
|
74
|
+
const gitRoot = await getGitRoot(projectDir);
|
|
75
|
+
const sessionId = slugify(name);
|
|
76
|
+
|
|
77
|
+
const session = await client.createSession({
|
|
78
|
+
id: sessionId,
|
|
79
|
+
name,
|
|
80
|
+
project_dir: projectDir,
|
|
81
|
+
git_root: gitRoot,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Write local session file
|
|
85
|
+
const sessionDir = path.join(projectDir, SESSION_DIR);
|
|
86
|
+
if (!fs.existsSync(sessionDir)) fs.mkdirSync(sessionDir, { recursive: true });
|
|
87
|
+
const sessionFile: SessionFile = {
|
|
88
|
+
session_id: session.id,
|
|
89
|
+
created_at: new Date().toISOString(),
|
|
90
|
+
broker_port: BROKER_PORT,
|
|
91
|
+
};
|
|
92
|
+
await Bun.write(path.join(projectDir, SESSION_FILE), JSON.stringify(sessionFile, null, 2));
|
|
93
|
+
|
|
94
|
+
console.log(`Session created: ${session.name} (${session.id})`);
|
|
95
|
+
console.log(`Wrote ${SESSION_FILE}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function list(client: BrokerClient): Promise<void> {
|
|
99
|
+
const sessions = await client.listSessions();
|
|
100
|
+
|
|
101
|
+
if (sessions.length === 0) {
|
|
102
|
+
console.log("No sessions found.");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const local = readLocalSession();
|
|
107
|
+
console.log("\n Sessions:\n");
|
|
108
|
+
|
|
109
|
+
for (const s of sessions) {
|
|
110
|
+
const active = local?.session_id === s.id ? " \x1b[36m(current)\x1b[0m" : "";
|
|
111
|
+
const statusColor = s.status === "active" ? "\x1b[32m" : s.status === "paused" ? "\x1b[33m" : "\x1b[90m";
|
|
112
|
+
console.log(` ${s.id}${active}`);
|
|
113
|
+
console.log(` Name: ${s.name}`);
|
|
114
|
+
console.log(` Status: ${statusColor}${s.status}\x1b[0m`);
|
|
115
|
+
console.log(` Dir: ${s.project_dir}`);
|
|
116
|
+
console.log(` Active: ${timeSince(s.last_active_at)}`);
|
|
117
|
+
console.log(` Created: ${formatTime(s.created_at)}`);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const slots = await client.listSlots(s.id);
|
|
121
|
+
const connected = slots.filter((sl) => sl.status === "connected").length;
|
|
122
|
+
console.log(` Agents: ${connected}/${slots.length} connected`);
|
|
123
|
+
} catch { /* broker may not support slots yet */ }
|
|
124
|
+
|
|
125
|
+
console.log();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function resume(client: BrokerClient, sessionId?: string): Promise<void> {
|
|
130
|
+
const id = sessionId ?? readLocalSession()?.session_id;
|
|
131
|
+
if (!id) {
|
|
132
|
+
console.error("No session specified and no local session found.");
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await client.updateSession({ id, status: "active", pause_reason: null, paused_at: null });
|
|
137
|
+
console.log(`Session "${id}" resumed.`);
|
|
138
|
+
|
|
139
|
+
// Check for disconnected slots and offer to relaunch
|
|
140
|
+
try {
|
|
141
|
+
const slots = await client.listSlots(id);
|
|
142
|
+
const disconnected = slots.filter((s) => s.status === "disconnected");
|
|
143
|
+
if (disconnected.length > 0) {
|
|
144
|
+
console.log(`\n${disconnected.length} agent(s) disconnected:`);
|
|
145
|
+
for (const s of disconnected) {
|
|
146
|
+
console.log(` Slot ${s.id}: ${s.display_name ?? s.agent_type} (${s.agent_type}) - ${s.role ?? "no role"}`);
|
|
147
|
+
}
|
|
148
|
+
const answer = await prompt("\nRelaunch disconnected agents? (y/n)");
|
|
149
|
+
if (answer.toLowerCase() === "y") {
|
|
150
|
+
await relaunchAgents(disconnected, id);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch { /* ok */ }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function relaunchAgents(slots: Array<{ id: number; agent_type: string; display_name: string | null; role: string | null }>, sessionId: string): Promise<void> {
|
|
157
|
+
const platform = process.platform;
|
|
158
|
+
|
|
159
|
+
for (const slot of slots) {
|
|
160
|
+
const cmd = slot.agent_type; // claude, codex, gemini
|
|
161
|
+
const envVars = `MULTIAGENTS_SESSION=${sessionId} MULTIAGENTS_ROLE=${slot.role ?? ""} MULTIAGENTS_NAME=${slot.display_name ?? ""}`;
|
|
162
|
+
try {
|
|
163
|
+
if (platform === "darwin") {
|
|
164
|
+
// macOS: open a new Terminal tab with session env vars
|
|
165
|
+
Bun.spawnSync([
|
|
166
|
+
"osascript", "-e",
|
|
167
|
+
`tell application "Terminal" to do script "${envVars} ${cmd}"`,
|
|
168
|
+
]);
|
|
169
|
+
} else {
|
|
170
|
+
// Linux: try gnome-terminal with session env vars
|
|
171
|
+
Bun.spawn(["gnome-terminal", "--", "env", `MULTIAGENTS_SESSION=${sessionId}`, `MULTIAGENTS_ROLE=${slot.role ?? ""}`, `MULTIAGENTS_NAME=${slot.display_name ?? ""}`, cmd], { stdio: ["ignore", "ignore", "ignore"] });
|
|
172
|
+
}
|
|
173
|
+
console.log(` Launched ${slot.display_name ?? slot.agent_type} in new terminal`);
|
|
174
|
+
} catch (e) {
|
|
175
|
+
console.error(` Failed to launch ${slot.agent_type}: ${e instanceof Error ? e.message : String(e)}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function pause(client: BrokerClient, sessionId?: string): Promise<void> {
|
|
181
|
+
const id = sessionId ?? readLocalSession()?.session_id;
|
|
182
|
+
if (!id) {
|
|
183
|
+
console.error("No session specified and no local session found.");
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await client.updateSession({
|
|
188
|
+
id,
|
|
189
|
+
status: "paused",
|
|
190
|
+
pause_reason: "Paused via CLI",
|
|
191
|
+
paused_at: Date.now(),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Pause all slots
|
|
195
|
+
try {
|
|
196
|
+
const slots = await client.listSlots(id);
|
|
197
|
+
for (const s of slots) {
|
|
198
|
+
await client.updateSlot({ id: s.id, paused: true, paused_at: Date.now() });
|
|
199
|
+
}
|
|
200
|
+
} catch { /* ok */ }
|
|
201
|
+
|
|
202
|
+
console.log(`Session "${id}" paused.`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function archive(client: BrokerClient, sessionId?: string): Promise<void> {
|
|
206
|
+
if (!sessionId) {
|
|
207
|
+
console.error("Usage: multiagents session archive <session-id>");
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
await client.updateSession({ id: sessionId, status: "archived" });
|
|
211
|
+
console.log(`Session "${sessionId}" archived.`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function deleteSession(client: BrokerClient, sessionId?: string): Promise<void> {
|
|
215
|
+
if (!sessionId) {
|
|
216
|
+
console.error("Usage: multiagents session delete <session-id>");
|
|
217
|
+
process.exit(1);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const answer = await prompt(`Delete session "${sessionId}" and all data? This cannot be undone. (yes/no)`);
|
|
221
|
+
if (answer !== "yes") {
|
|
222
|
+
console.log("Cancelled.");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Archive first (soft delete via broker — actual delete if broker supports it)
|
|
227
|
+
try {
|
|
228
|
+
await client.updateSession({ id: sessionId, status: "archived" });
|
|
229
|
+
} catch { /* ok */ }
|
|
230
|
+
|
|
231
|
+
// Clean local session file if it matches
|
|
232
|
+
const local = readLocalSession();
|
|
233
|
+
if (local?.session_id === sessionId) {
|
|
234
|
+
const sessionFilePath = path.resolve(process.cwd(), SESSION_FILE);
|
|
235
|
+
if (fs.existsSync(sessionFilePath)) fs.unlinkSync(sessionFilePath);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
console.log(`Session "${sessionId}" deleted.`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function exportSession(client: BrokerClient, sessionId?: string): Promise<void> {
|
|
242
|
+
const id = sessionId ?? readLocalSession()?.session_id;
|
|
243
|
+
if (!id) {
|
|
244
|
+
console.error("Usage: multiagents session export <session-id>");
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const session = await client.getSession(id);
|
|
249
|
+
const messages = await client.getMessageLog(id, { limit: 10000 });
|
|
250
|
+
const slots = await client.listSlots(id);
|
|
251
|
+
|
|
252
|
+
// Build slot lookup
|
|
253
|
+
const slotMap = new Map(slots.map((s) => [s.id, s]));
|
|
254
|
+
|
|
255
|
+
// Format as markdown
|
|
256
|
+
let md = `# Session: ${session.name}\n\n`;
|
|
257
|
+
md += `- **ID:** ${session.id}\n`;
|
|
258
|
+
md += `- **Status:** ${session.status}\n`;
|
|
259
|
+
md += `- **Directory:** ${session.project_dir}\n`;
|
|
260
|
+
md += `- **Created:** ${new Date(session.created_at).toISOString()}\n\n`;
|
|
261
|
+
|
|
262
|
+
md += `## Agents\n\n`;
|
|
263
|
+
for (const s of slots) {
|
|
264
|
+
md += `- **Slot ${s.id}:** ${s.display_name ?? "unnamed"} (${s.agent_type}) — ${s.role ?? "no role"}\n`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
md += `\n## Message Log\n\n`;
|
|
268
|
+
for (const m of messages) {
|
|
269
|
+
const fromSlot = m.from_slot_id !== null ? slotMap.get(m.from_slot_id) : null;
|
|
270
|
+
const fromName = fromSlot?.display_name ?? m.from_id;
|
|
271
|
+
const time = formatTime(m.sent_at);
|
|
272
|
+
md += `**[${time}] ${fromName}** (${m.msg_type}):\n${m.text}\n\n---\n\n`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const filename = `session-${id}-export.md`;
|
|
276
|
+
await Bun.write(filename, md);
|
|
277
|
+
console.log(`Exported ${messages.length} messages to ${filename}`);
|
|
278
|
+
}
|
package/cli/setup.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// multiagents — Interactive Setup Wizard
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { DEFAULT_BROKER_PORT, BROKER_HOSTNAME, SESSION_DIR, SESSION_FILE } from "../shared/constants.ts";
|
|
6
|
+
import { BrokerClient } from "../shared/broker-client.ts";
|
|
7
|
+
import { expandHome, getGitRoot, slugify } from "../shared/utils.ts";
|
|
8
|
+
import type { AgentType, SessionFile } from "../shared/types.ts";
|
|
9
|
+
import * as readline from "node:readline";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import * as fs from "node:fs";
|
|
12
|
+
|
|
13
|
+
const BROKER_PORT = parseInt(process.env.MULTIAGENTS_PORT ?? String(DEFAULT_BROKER_PORT), 10);
|
|
14
|
+
const BROKER_URL = `http://${BROKER_HOSTNAME}:${BROKER_PORT}`;
|
|
15
|
+
|
|
16
|
+
// --- Helpers ---
|
|
17
|
+
|
|
18
|
+
function prompt(question: string, defaultValue?: string): Promise<string> {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
21
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
22
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
23
|
+
rl.close();
|
|
24
|
+
resolve(answer.trim() || defaultValue || "");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function detectAgent(name: string): { available: boolean; version?: string } {
|
|
30
|
+
try {
|
|
31
|
+
const which = Bun.spawnSync(["which", name]);
|
|
32
|
+
if (which.exitCode !== 0) return { available: false };
|
|
33
|
+
|
|
34
|
+
const ver = Bun.spawnSync([name, "--version"]);
|
|
35
|
+
const version = new TextDecoder().decode(ver.stdout).trim().split("\n")[0];
|
|
36
|
+
return { available: true, version: version || undefined };
|
|
37
|
+
} catch {
|
|
38
|
+
return { available: false };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function setup(): Promise<void> {
|
|
43
|
+
// 1. Header banner
|
|
44
|
+
console.log(`
|
|
45
|
+
\x1b[1m\x1b[36m multiagents\x1b[0m
|
|
46
|
+
\x1b[90m Interactive Setup Wizard\x1b[0m
|
|
47
|
+
\x1b[90m ─────────────────────────────────\x1b[0m
|
|
48
|
+
`);
|
|
49
|
+
|
|
50
|
+
// 2. Detect installed agents
|
|
51
|
+
console.log("\x1b[1mDetecting installed agents...\x1b[0m\n");
|
|
52
|
+
const agents: { type: AgentType; name: string; cmd: string; info: ReturnType<typeof detectAgent> }[] = [
|
|
53
|
+
{ type: "claude", name: "Claude Code", cmd: "claude", info: detectAgent("claude") },
|
|
54
|
+
{ type: "codex", name: "Codex CLI", cmd: "codex", info: detectAgent("codex") },
|
|
55
|
+
{ type: "gemini", name: "Gemini CLI", cmd: "gemini", info: detectAgent("gemini") },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
for (const a of agents) {
|
|
59
|
+
const icon = a.info.available ? "\x1b[32m✔\x1b[0m" : "\x1b[31m✗\x1b[0m";
|
|
60
|
+
const ver = a.info.version ? ` \x1b[90m(${a.info.version})\x1b[0m` : "";
|
|
61
|
+
console.log(` ${icon} ${a.name}${ver}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const available = agents.filter((a) => a.info.available);
|
|
65
|
+
if (available.length === 0) {
|
|
66
|
+
console.error("\n\x1b[31mNo supported agents found. Install at least one agent CLI first.\x1b[0m");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 3. Prompt user to select agents
|
|
71
|
+
console.log("\n\x1b[1mSelect agents to orchestrate:\x1b[0m\n");
|
|
72
|
+
for (let i = 0; i < available.length; i++) {
|
|
73
|
+
console.log(` ${i + 1}. ${available[i].name} (${available[i].cmd})`);
|
|
74
|
+
}
|
|
75
|
+
console.log(` a. All available agents`);
|
|
76
|
+
|
|
77
|
+
const selection = await prompt("\nEnter numbers separated by commas, or 'a' for all", "a");
|
|
78
|
+
let selected: typeof available;
|
|
79
|
+
if (selection === "a" || selection === "A") {
|
|
80
|
+
selected = available;
|
|
81
|
+
} else {
|
|
82
|
+
const indices = selection.split(",").map((s) => parseInt(s.trim(), 10) - 1);
|
|
83
|
+
selected = indices
|
|
84
|
+
.filter((i) => i >= 0 && i < available.length)
|
|
85
|
+
.map((i) => available[i]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (selected.length === 0) {
|
|
89
|
+
console.error("\n\x1b[31mNo agents selected.\x1b[0m");
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
console.log(`\nSelected: ${selected.map((a) => a.name).join(", ")}`);
|
|
93
|
+
|
|
94
|
+
// 4. Prompt for working directory
|
|
95
|
+
const projectDir = await prompt("\nProject directory", process.cwd());
|
|
96
|
+
const resolvedDir = path.resolve(expandHome(projectDir));
|
|
97
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
98
|
+
console.error(`\n\x1b[31mDirectory does not exist: ${resolvedDir}\x1b[0m`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 5. Prompt for session name
|
|
103
|
+
const dirName = path.basename(resolvedDir);
|
|
104
|
+
const sessionName = await prompt("Session name", dirName);
|
|
105
|
+
const sessionId = slugify(sessionName);
|
|
106
|
+
|
|
107
|
+
// 6. Configure each selected agent's MCP server
|
|
108
|
+
console.log("\n\x1b[1mConfiguring MCP servers...\x1b[0m\n");
|
|
109
|
+
|
|
110
|
+
const cliPath = path.resolve(import.meta.dir, "..");
|
|
111
|
+
|
|
112
|
+
for (const agent of selected) {
|
|
113
|
+
try {
|
|
114
|
+
switch (agent.type) {
|
|
115
|
+
case "claude":
|
|
116
|
+
await configureClaudeMcp(resolvedDir, cliPath);
|
|
117
|
+
break;
|
|
118
|
+
case "codex":
|
|
119
|
+
await configureCodexMcp(resolvedDir, cliPath);
|
|
120
|
+
break;
|
|
121
|
+
case "gemini":
|
|
122
|
+
await configureGeminiMcp(cliPath);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
console.log(` \x1b[32m✔\x1b[0m ${agent.name} MCP configured`);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.error(` \x1b[31m✗\x1b[0m ${agent.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 7. Start broker
|
|
132
|
+
console.log("\n\x1b[1mStarting broker...\x1b[0m");
|
|
133
|
+
const client = new BrokerClient(BROKER_URL);
|
|
134
|
+
let brokerAlive = await client.isAlive();
|
|
135
|
+
if (!brokerAlive) {
|
|
136
|
+
const proc = Bun.spawn(["bun", path.resolve(cliPath, "broker.ts")], {
|
|
137
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
138
|
+
});
|
|
139
|
+
proc.unref();
|
|
140
|
+
for (let i = 0; i < 30; i++) {
|
|
141
|
+
if (await client.isAlive()) { brokerAlive = true; break; }
|
|
142
|
+
await Bun.sleep(200);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!brokerAlive) {
|
|
146
|
+
console.error(" \x1b[31m✗\x1b[0m Broker failed to start");
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
console.log(` \x1b[32m✔\x1b[0m Broker running on ${BROKER_URL}`);
|
|
150
|
+
|
|
151
|
+
// 8. Create session
|
|
152
|
+
console.log("\n\x1b[1mCreating session...\x1b[0m");
|
|
153
|
+
const gitRoot = await getGitRoot(resolvedDir);
|
|
154
|
+
try {
|
|
155
|
+
await client.createSession({
|
|
156
|
+
id: sessionId,
|
|
157
|
+
name: sessionName,
|
|
158
|
+
project_dir: resolvedDir,
|
|
159
|
+
git_root: gitRoot,
|
|
160
|
+
});
|
|
161
|
+
console.log(` \x1b[32m✔\x1b[0m Session "${sessionName}" (${sessionId}) created`);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
164
|
+
if (msg.includes("409") || msg.includes("UNIQUE") || msg.includes("already")) {
|
|
165
|
+
console.log(` \x1b[33m!\x1b[0m Session "${sessionId}" already exists, reusing`);
|
|
166
|
+
} else {
|
|
167
|
+
throw e;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 9. Write session.json
|
|
172
|
+
const sessionDir = path.join(resolvedDir, SESSION_DIR);
|
|
173
|
+
if (!fs.existsSync(sessionDir)) fs.mkdirSync(sessionDir, { recursive: true });
|
|
174
|
+
|
|
175
|
+
const sessionFile: SessionFile = {
|
|
176
|
+
session_id: sessionId,
|
|
177
|
+
created_at: new Date().toISOString(),
|
|
178
|
+
broker_port: BROKER_PORT,
|
|
179
|
+
};
|
|
180
|
+
await Bun.write(path.join(resolvedDir, SESSION_FILE), JSON.stringify(sessionFile, null, 2));
|
|
181
|
+
console.log(` \x1b[32m✔\x1b[0m Wrote ${SESSION_FILE}`);
|
|
182
|
+
|
|
183
|
+
// 10. Print next steps
|
|
184
|
+
console.log(`
|
|
185
|
+
\x1b[1m\x1b[32mSetup complete!\x1b[0m
|
|
186
|
+
|
|
187
|
+
\x1b[1mNext steps:\x1b[0m
|
|
188
|
+
1. Open your agents in the project directory:
|
|
189
|
+
\x1b[90mcd ${resolvedDir}\x1b[0m
|
|
190
|
+
${selected.map((a) => ` \x1b[90m${a.cmd}\x1b[0m`).join("\n")}
|
|
191
|
+
|
|
192
|
+
2. Each agent will auto-connect to the session via MCP.
|
|
193
|
+
|
|
194
|
+
3. Monitor with the dashboard:
|
|
195
|
+
\x1b[90mmultiagents dashboard\x1b[0m
|
|
196
|
+
|
|
197
|
+
4. Or use the orchestrator for automated coordination:
|
|
198
|
+
\x1b[90mmultiagents orchestrator\x1b[0m
|
|
199
|
+
`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// --- Agent MCP configuration ---
|
|
203
|
+
|
|
204
|
+
async function configureClaudeMcp(projectDir: string, cliPath: string): Promise<void> {
|
|
205
|
+
// Write project-level .mcp.json
|
|
206
|
+
const mcpPath = path.join(projectDir, ".mcp.json");
|
|
207
|
+
let config: Record<string, unknown> = {};
|
|
208
|
+
try {
|
|
209
|
+
const existing = await Bun.file(mcpPath).text();
|
|
210
|
+
config = JSON.parse(existing);
|
|
211
|
+
} catch { /* file doesn't exist */ }
|
|
212
|
+
|
|
213
|
+
const mcpServers = (config.mcpServers as Record<string, unknown>) ?? {};
|
|
214
|
+
mcpServers["multiagents"] = {
|
|
215
|
+
command: "bun",
|
|
216
|
+
args: [path.resolve(cliPath, "cli.ts"), "mcp-server", "--agent-type", "claude"],
|
|
217
|
+
};
|
|
218
|
+
config.mcpServers = mcpServers;
|
|
219
|
+
await Bun.write(mcpPath, JSON.stringify(config, null, 2));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function configureCodexMcp(projectDir: string, cliPath: string): Promise<void> {
|
|
223
|
+
// Write .codex/config.toml
|
|
224
|
+
const codexDir = path.join(projectDir, ".codex");
|
|
225
|
+
if (!fs.existsSync(codexDir)) fs.mkdirSync(codexDir, { recursive: true });
|
|
226
|
+
|
|
227
|
+
const tomlPath = path.join(codexDir, "config.toml");
|
|
228
|
+
let existing = "";
|
|
229
|
+
try { existing = await Bun.file(tomlPath).text(); } catch { /* ok */ }
|
|
230
|
+
|
|
231
|
+
// Remove any existing multiagents section
|
|
232
|
+
existing = existing.replace(/\[mcp_servers\.multiagents\][\s\S]*?(?=\n\[|$)/, "").trim();
|
|
233
|
+
|
|
234
|
+
const entry = `\n\n[mcp_servers.multiagents]\ncommand = "bun"\nargs = [${JSON.stringify(path.resolve(cliPath, "cli.ts"))}, "mcp-server", "--agent-type", "codex"]\n`;
|
|
235
|
+
await Bun.write(tomlPath, existing + entry);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function configureGeminiMcp(cliPath: string): Promise<void> {
|
|
239
|
+
// Write ~/.gemini/settings.json
|
|
240
|
+
const geminiDir = expandHome("~/.gemini");
|
|
241
|
+
if (!fs.existsSync(geminiDir)) fs.mkdirSync(geminiDir, { recursive: true });
|
|
242
|
+
|
|
243
|
+
const settingsPath = path.join(geminiDir, "settings.json");
|
|
244
|
+
let config: Record<string, unknown> = {};
|
|
245
|
+
try {
|
|
246
|
+
const existing = await Bun.file(settingsPath).text();
|
|
247
|
+
config = JSON.parse(existing);
|
|
248
|
+
} catch { /* ok */ }
|
|
249
|
+
|
|
250
|
+
const mcpServers = (config.mcpServers as Record<string, unknown>) ?? {};
|
|
251
|
+
mcpServers["multiagents"] = {
|
|
252
|
+
command: "bun",
|
|
253
|
+
args: [path.resolve(cliPath, "cli.ts"), "mcp-server", "--agent-type", "gemini"],
|
|
254
|
+
};
|
|
255
|
+
config.mcpServers = mcpServers;
|
|
256
|
+
await Bun.write(settingsPath, JSON.stringify(config, null, 2));
|
|
257
|
+
}
|
package/cli.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// ============================================================================
|
|
3
|
+
// multiagents — CLI Entry Point
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Routes to the new modular CLI. Maintains backward compatibility with
|
|
6
|
+
// the original cli.ts commands (status, peers, send, kill-broker).
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
import { runCli } from "./cli/commands.ts";
|
|
10
|
+
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
|
|
13
|
+
// Backward compatibility: old cli.ts accepted these directly
|
|
14
|
+
// The new router handles them natively, so just pass through.
|
|
15
|
+
// "kill-broker" is explicitly aliased in commands.ts.
|
|
16
|
+
|
|
17
|
+
await runCli(args);
|
package/index.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* multiagents
|
|
3
|
+
*
|
|
4
|
+
* Multi-agent orchestration platform for Claude Code, Codex CLI, and Gemini CLI.
|
|
5
|
+
*
|
|
6
|
+
* Entry points:
|
|
7
|
+
* - server.ts — MCP server (one per agent instance)
|
|
8
|
+
* - broker.ts — Shared broker daemon (one per machine)
|
|
9
|
+
* - orchestrator/orchestrator-server.ts — Orchestrator MCP (for Claude Desktop)
|
|
10
|
+
* - cli.ts — CLI tool (setup, dashboard, session mgmt)
|
|
11
|
+
*
|
|
12
|
+
* See README.md for setup and usage.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
PeerId,
|
|
17
|
+
AgentType,
|
|
18
|
+
MessageType,
|
|
19
|
+
SessionStatus,
|
|
20
|
+
Peer,
|
|
21
|
+
Message,
|
|
22
|
+
Session,
|
|
23
|
+
Slot,
|
|
24
|
+
FileLock,
|
|
25
|
+
FileOwnership,
|
|
26
|
+
Guardrail,
|
|
27
|
+
GuardrailState,
|
|
28
|
+
BufferedMessage,
|
|
29
|
+
SessionFile,
|
|
30
|
+
AgentLaunchConfig,
|
|
31
|
+
TeamConfig,
|
|
32
|
+
} from "./shared/types.ts";
|
|
33
|
+
|
|
34
|
+
export { BrokerClient } from "./shared/broker-client.ts";
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
DEFAULT_BROKER_PORT,
|
|
38
|
+
DEFAULT_GUARDRAILS,
|
|
39
|
+
POLL_INTERVALS,
|
|
40
|
+
SESSION_FILE,
|
|
41
|
+
} from "./shared/constants.ts";
|
package/noop-mcp.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* No-op MCP server — responds to the JSON-RPC initialize handshake instantly,
|
|
4
|
+
* reports zero tools, then stays alive reading stdin until the client disconnects.
|
|
5
|
+
*
|
|
6
|
+
* Used to neutralize unwanted global MCP server entries in Codex config without
|
|
7
|
+
* causing 10s handshake timeouts (like /usr/bin/true does) or deserialization
|
|
8
|
+
* errors (like `echo disabled` does).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const decoder = new TextDecoder();
|
|
12
|
+
let buffer = "";
|
|
13
|
+
|
|
14
|
+
process.stdin.on("data", (chunk: Buffer) => {
|
|
15
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
16
|
+
|
|
17
|
+
// Process complete lines (JSON-RPC uses newline-delimited JSON)
|
|
18
|
+
const lines = buffer.split("\n");
|
|
19
|
+
buffer = lines.pop() ?? "";
|
|
20
|
+
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (!trimmed) continue;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const msg = JSON.parse(trimmed);
|
|
27
|
+
|
|
28
|
+
if (msg.method === "initialize") {
|
|
29
|
+
const response = JSON.stringify({
|
|
30
|
+
jsonrpc: "2.0",
|
|
31
|
+
id: msg.id,
|
|
32
|
+
result: {
|
|
33
|
+
protocolVersion: "2024-11-05",
|
|
34
|
+
capabilities: {},
|
|
35
|
+
serverInfo: { name: "noop", version: "0.0.0" },
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
process.stdout.write(response + "\n");
|
|
39
|
+
} else if (msg.method === "notifications/initialized") {
|
|
40
|
+
// Client acknowledged — nothing to do
|
|
41
|
+
} else if (msg.method === "tools/list") {
|
|
42
|
+
const response = JSON.stringify({
|
|
43
|
+
jsonrpc: "2.0",
|
|
44
|
+
id: msg.id,
|
|
45
|
+
result: { tools: [] },
|
|
46
|
+
});
|
|
47
|
+
process.stdout.write(response + "\n");
|
|
48
|
+
} else if (msg.id !== undefined) {
|
|
49
|
+
// Unknown request — respond with empty result
|
|
50
|
+
const response = JSON.stringify({
|
|
51
|
+
jsonrpc: "2.0",
|
|
52
|
+
id: msg.id,
|
|
53
|
+
result: {},
|
|
54
|
+
});
|
|
55
|
+
process.stdout.write(response + "\n");
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// Not JSON — ignore
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
process.stdin.on("end", () => process.exit(0));
|