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.
@@ -0,0 +1,343 @@
1
+ // ============================================================================
2
+ // multiagents — Session Control
3
+ // ============================================================================
4
+ // Pause/resume agents, broadcast messages, resolve agent targets, and
5
+ // dispatch control actions for the orchestrator.
6
+ // ============================================================================
7
+
8
+ import type { Slot } from "../shared/types.ts";
9
+ import type { BrokerClient } from "../shared/broker-client.ts";
10
+ import { log } from "../shared/utils.ts";
11
+ import { resumeAfterGuardrailAdjusted } from "./guardrails.ts";
12
+ import { getTeamStatus, formatTeamStatusForDisplay } from "./progress.ts";
13
+
14
+ const LOG_PREFIX = "session-ctrl";
15
+
16
+ /** Result of a control action. */
17
+ export interface ControlResult {
18
+ status: string;
19
+ affected?: number;
20
+ message: string;
21
+ agents?: any[];
22
+ warnings?: any[];
23
+ guardrail?: any;
24
+ }
25
+
26
+ /**
27
+ * Main dispatch function for session control actions.
28
+ */
29
+ export async function controlSession(
30
+ sessionId: string,
31
+ action: string,
32
+ brokerClient: BrokerClient,
33
+ target?: string,
34
+ value?: number,
35
+ ): Promise<ControlResult> {
36
+ switch (action) {
37
+ case "pause_all":
38
+ return await pauseAll(sessionId, brokerClient);
39
+
40
+ case "resume_all":
41
+ return await resumeAll(sessionId, brokerClient);
42
+
43
+ case "pause_agent": {
44
+ if (!target) return { status: "error", message: "Target agent required for pause_agent" };
45
+ const slot = await resolveTarget(sessionId, target, brokerClient);
46
+ if (!slot) return { status: "error", message: `Could not find agent matching "${target}"` };
47
+ await pauseAgent(sessionId, slot, brokerClient);
48
+ return { status: "ok", affected: 1, message: `Paused ${slot.display_name ?? `slot ${slot.id}`}` };
49
+ }
50
+
51
+ case "resume_agent": {
52
+ if (!target) return { status: "error", message: "Target agent required for resume_agent" };
53
+ const slot = await resolveTarget(sessionId, target, brokerClient);
54
+ if (!slot) return { status: "error", message: `Could not find agent matching "${target}"` };
55
+ await resumeAgent(sessionId, slot, brokerClient);
56
+ return { status: "ok", affected: 1, message: `Resumed ${slot.display_name ?? `slot ${slot.id}`}` };
57
+ }
58
+
59
+ case "extend_budget": {
60
+ if (value === undefined) return { status: "error", message: "Value required for extend_budget" };
61
+ // Extend the session_duration guardrail by the given value
62
+ const guardrails = await brokerClient.getGuardrails(sessionId);
63
+ const duration = guardrails.find((g) => g.id === "session_duration");
64
+ if (!duration) return { status: "error", message: "Session duration guardrail not found" };
65
+
66
+ const newValue = duration.current_value + value;
67
+ const updated = await brokerClient.updateGuardrail({
68
+ session_id: sessionId,
69
+ guardrail_id: "session_duration",
70
+ new_value: newValue,
71
+ changed_by: "orchestrator",
72
+ reason: `Extended by ${value} ${duration.unit}`,
73
+ });
74
+
75
+ // If session was paused due to this guardrail, resume
76
+ const session = await brokerClient.getSession(sessionId);
77
+ if (session.status === "paused" && session.pause_reason?.includes("session_duration")) {
78
+ await resumeAfterGuardrailAdjusted(sessionId, brokerClient);
79
+ }
80
+
81
+ return {
82
+ status: "ok",
83
+ message: `Extended session duration to ${newValue} ${duration.unit}`,
84
+ guardrail: updated,
85
+ };
86
+ }
87
+
88
+ case "set_budget": {
89
+ if (!target) return { status: "error", message: "Target guardrail_id required" };
90
+ if (value === undefined) return { status: "error", message: "Value required for set_budget" };
91
+
92
+ const updated = await brokerClient.updateGuardrail({
93
+ session_id: sessionId,
94
+ guardrail_id: target,
95
+ new_value: value,
96
+ changed_by: "orchestrator",
97
+ });
98
+
99
+ // Check if this resolves a pause
100
+ const session = await brokerClient.getSession(sessionId);
101
+ if (session.status === "paused" && session.pause_reason?.includes(target)) {
102
+ await resumeAfterGuardrailAdjusted(sessionId, brokerClient);
103
+ }
104
+
105
+ return {
106
+ status: "ok",
107
+ message: `Set ${target} to ${value}`,
108
+ guardrail: updated,
109
+ };
110
+ }
111
+
112
+ case "status": {
113
+ const teamStatus = await getTeamStatus(sessionId, brokerClient);
114
+ const display = formatTeamStatusForDisplay(teamStatus);
115
+ return {
116
+ status: "ok",
117
+ message: display,
118
+ agents: teamStatus.agents,
119
+ warnings: teamStatus.issues,
120
+ };
121
+ }
122
+
123
+ default:
124
+ return { status: "error", message: `Unknown action: ${action}` };
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Pause a single agent slot. Sends a control message, holds future messages.
130
+ */
131
+ export async function pauseAgent(
132
+ sessionId: string,
133
+ slot: Slot,
134
+ brokerClient: BrokerClient,
135
+ ): Promise<void> {
136
+ // Update slot state
137
+ await brokerClient.updateSlot({
138
+ id: slot.id,
139
+ paused: true,
140
+ paused_at: Date.now(),
141
+ });
142
+
143
+ // Hold incoming messages
144
+ await brokerClient.holdMessages(sessionId, slot.id);
145
+
146
+ // Release file locks held by this agent
147
+ try {
148
+ const locks = await brokerClient.listFileLocks(sessionId);
149
+ for (const lock of locks) {
150
+ if (lock.held_by_slot === slot.id) {
151
+ await brokerClient.releaseFile({
152
+ session_id: sessionId,
153
+ peer_id: lock.held_by_peer,
154
+ file_path: lock.file_path,
155
+ });
156
+ }
157
+ }
158
+ } catch {
159
+ // Best-effort lock release
160
+ }
161
+
162
+ // Send control message to the agent
163
+ if (slot.peer_id) {
164
+ await brokerClient.sendMessage({
165
+ from_id: "orchestrator",
166
+ to_id: slot.peer_id,
167
+ text: JSON.stringify({ action: "pause", reason: "Paused by orchestrator" }),
168
+ msg_type: "control",
169
+ session_id: sessionId,
170
+ });
171
+ }
172
+
173
+ log(LOG_PREFIX, `Paused agent ${slot.display_name ?? slot.id}`);
174
+ }
175
+
176
+ /**
177
+ * Resume a paused agent. Sends resume with any held messages + team changes.
178
+ */
179
+ export async function resumeAgent(
180
+ sessionId: string,
181
+ slot: Slot,
182
+ brokerClient: BrokerClient,
183
+ ): Promise<void> {
184
+ // Update slot state
185
+ await brokerClient.updateSlot({
186
+ id: slot.id,
187
+ paused: false,
188
+ paused_at: null,
189
+ });
190
+
191
+ // Release held messages
192
+ await brokerClient.releaseHeldMessages(sessionId, slot.id);
193
+
194
+ // Send resume control message
195
+ if (slot.peer_id) {
196
+ await brokerClient.sendMessage({
197
+ from_id: "orchestrator",
198
+ to_id: slot.peer_id,
199
+ text: JSON.stringify({
200
+ action: "resume",
201
+ reason: "Resumed by orchestrator",
202
+ }),
203
+ msg_type: "control",
204
+ session_id: sessionId,
205
+ });
206
+ }
207
+
208
+ log(LOG_PREFIX, `Resumed agent ${slot.display_name ?? slot.id}`);
209
+ }
210
+
211
+ /**
212
+ * Resolve a target string to a Slot. Supports matching by:
213
+ * - Exact slot ID (number)
214
+ * - Exact display name
215
+ * - Exact role name
216
+ * - Partial/fuzzy match on name or role
217
+ */
218
+ export async function resolveTarget(
219
+ sessionId: string,
220
+ target: string,
221
+ brokerClient: BrokerClient,
222
+ ): Promise<Slot | null> {
223
+ const slots = await brokerClient.listSlots(sessionId);
224
+ const lower = target.toLowerCase();
225
+
226
+ // Try exact slot ID
227
+ const asNum = parseInt(target, 10);
228
+ if (!isNaN(asNum)) {
229
+ const byId = slots.find((s) => s.id === asNum);
230
+ if (byId) return byId;
231
+ }
232
+
233
+ // Try exact name match
234
+ const byName = slots.find(
235
+ (s) => s.display_name?.toLowerCase() === lower,
236
+ );
237
+ if (byName) return byName;
238
+
239
+ // Try exact role match
240
+ const byRole = slots.find((s) => s.role?.toLowerCase() === lower);
241
+ if (byRole) return byRole;
242
+
243
+ // Partial match on name
244
+ const partialName = slots.find(
245
+ (s) => s.display_name?.toLowerCase().includes(lower),
246
+ );
247
+ if (partialName) return partialName;
248
+
249
+ // Partial match on role
250
+ const partialRole = slots.find(
251
+ (s) => s.role?.toLowerCase().includes(lower),
252
+ );
253
+ if (partialRole) return partialRole;
254
+
255
+ return null;
256
+ }
257
+
258
+ /**
259
+ * Broadcast a message to all connected slots in the session.
260
+ */
261
+ export async function broadcastToTeam(
262
+ sessionId: string,
263
+ message: string,
264
+ brokerClient: BrokerClient,
265
+ excludeRoles?: string[],
266
+ ): Promise<{ delivered_to: number }> {
267
+ const slots = await brokerClient.listSlots(sessionId);
268
+ const excludeSet = new Set((excludeRoles ?? []).map((r) => r.toLowerCase()));
269
+
270
+ let deliveredTo = 0;
271
+
272
+ for (const slot of slots) {
273
+ // Skip disconnected or paused agents
274
+ if (slot.status !== "connected" || slot.paused) continue;
275
+ if (!slot.peer_id) continue;
276
+
277
+ // Skip excluded roles
278
+ if (slot.role && excludeSet.has(slot.role.toLowerCase())) continue;
279
+
280
+ await brokerClient.sendMessage({
281
+ from_id: "orchestrator",
282
+ to_id: slot.peer_id,
283
+ text: message,
284
+ msg_type: "broadcast",
285
+ session_id: sessionId,
286
+ });
287
+
288
+ deliveredTo++;
289
+ }
290
+
291
+ log(LOG_PREFIX, `Broadcast to ${deliveredTo} agents`);
292
+ return { delivered_to: deliveredTo };
293
+ }
294
+
295
+ /** Pause all agents in a session. */
296
+ async function pauseAll(
297
+ sessionId: string,
298
+ brokerClient: BrokerClient,
299
+ ): Promise<ControlResult> {
300
+ await brokerClient.updateSession({
301
+ id: sessionId,
302
+ status: "paused",
303
+ pause_reason: "Paused by orchestrator",
304
+ paused_at: Date.now(),
305
+ });
306
+
307
+ const slots = await brokerClient.listSlots(sessionId);
308
+ let affected = 0;
309
+
310
+ for (const slot of slots) {
311
+ if (slot.status === "connected" && !slot.paused) {
312
+ await pauseAgent(sessionId, slot, brokerClient);
313
+ affected++;
314
+ }
315
+ }
316
+
317
+ return { status: "ok", affected, message: `Paused ${affected} agents` };
318
+ }
319
+
320
+ /** Resume all agents in a session. */
321
+ async function resumeAll(
322
+ sessionId: string,
323
+ brokerClient: BrokerClient,
324
+ ): Promise<ControlResult> {
325
+ await brokerClient.updateSession({
326
+ id: sessionId,
327
+ status: "active",
328
+ pause_reason: null,
329
+ paused_at: null,
330
+ });
331
+
332
+ const slots = await brokerClient.listSlots(sessionId);
333
+ let affected = 0;
334
+
335
+ for (const slot of slots) {
336
+ if (slot.paused) {
337
+ await resumeAgent(sessionId, slot, brokerClient);
338
+ affected++;
339
+ }
340
+ }
341
+
342
+ return { status: "ok", affected, message: `Resumed ${affected} agents` };
343
+ }
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "multiagents",
3
+ "version": "0.1.0",
4
+ "description": "Multi-agent orchestration platform for Claude Code, Codex CLI, and Gemini CLI",
5
+ "module": "index.ts",
6
+ "type": "module",
7
+ "private": false,
8
+ "bin": {
9
+ "multiagents": "./cli.ts",
10
+ "multiagents-server": "./server.ts",
11
+ "multiagents-orch": "./orchestrator/orchestrator-server.ts",
12
+ "multiagents-broker": "./broker.ts"
13
+ },
14
+ "files": [
15
+ "cli.ts",
16
+ "server.ts",
17
+ "broker.ts",
18
+ "index.ts",
19
+ "noop-mcp.ts",
20
+ "shared/",
21
+ "adapters/",
22
+ "orchestrator/",
23
+ "cli/",
24
+ ".mcp.json",
25
+ "README.md",
26
+ "LICENSE",
27
+ "tsconfig.json",
28
+ "scripts/"
29
+ ],
30
+ "scripts": {
31
+ "broker": "bun broker.ts",
32
+ "server": "bun server.ts",
33
+ "orchestrator": "bun orchestrator/orchestrator-server.ts",
34
+ "setup": "bun cli.ts setup",
35
+ "dashboard": "bun cli.ts dashboard",
36
+ "test": "bun test",
37
+ "version:patch": "bun run scripts/version.ts patch",
38
+ "version:minor": "bun run scripts/version.ts minor",
39
+ "version:major": "bun run scripts/version.ts major",
40
+ "prepack": "bun test",
41
+ "postinstall": "bun run scripts/postinstall.ts 2>/dev/null || true"
42
+ },
43
+ "engines": {
44
+ "bun": ">=1.1.0"
45
+ },
46
+ "devDependencies": {
47
+ "@types/bun": "^1.2.12"
48
+ },
49
+ "peerDependencies": {
50
+ "typescript": "^5"
51
+ },
52
+ "dependencies": {
53
+ "@modelcontextprotocol/sdk": "^1.27.1"
54
+ },
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "https://github.com/zetbrush/multiagents"
58
+ },
59
+ "keywords": [
60
+ "mcp",
61
+ "multi-agent",
62
+ "claude",
63
+ "codex",
64
+ "gemini",
65
+ "orchestration",
66
+ "ai-agents"
67
+ ],
68
+ "license": "MIT",
69
+ "author": "Arman Andreasyan"
70
+ }
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Post-install script: auto-configures MCP servers for Claude Code.
4
+ *
5
+ * Adds multiagents and multiagents-orch to ~/.claude/.mcp.json
6
+ * so Claude Code can discover the tools without manual configuration.
7
+ *
8
+ * Safe: only adds entries if they don't already exist. Never overwrites.
9
+ */
10
+
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import * as os from "node:os";
14
+
15
+ const HOME = os.homedir();
16
+ const CLAUDE_DIR = path.join(HOME, ".claude");
17
+ const MCP_JSON = path.join(CLAUDE_DIR, ".mcp.json");
18
+
19
+ // Resolve the installed package's bin paths
20
+ const PKG_ROOT = path.resolve(import.meta.dir, "..");
21
+ const SERVER_PATH = path.join(PKG_ROOT, "server.ts");
22
+ const ORCH_PATH = path.join(PKG_ROOT, "orchestrator", "orchestrator-server.ts");
23
+
24
+ // Find bun binary
25
+ function findBun(): string {
26
+ const bunInPath = Bun.spawnSync(["which", "bun"]).stdout.toString().trim();
27
+ if (bunInPath) return bunInPath;
28
+ const defaultBun = path.join(HOME, ".bun", "bin", "bun");
29
+ if (fs.existsSync(defaultBun)) return defaultBun;
30
+ return "bun"; // fallback — hope it's in PATH at runtime
31
+ }
32
+
33
+ const BUN = findBun();
34
+
35
+ try {
36
+ // Ensure ~/.claude/ exists
37
+ if (!fs.existsSync(CLAUDE_DIR)) {
38
+ fs.mkdirSync(CLAUDE_DIR, { recursive: true });
39
+ }
40
+
41
+ // Read or create .mcp.json
42
+ let config: { mcpServers: Record<string, unknown> } = { mcpServers: {} };
43
+ if (fs.existsSync(MCP_JSON)) {
44
+ try {
45
+ config = JSON.parse(fs.readFileSync(MCP_JSON, "utf-8"));
46
+ if (!config.mcpServers) config.mcpServers = {};
47
+ } catch {
48
+ // Corrupted file — back up and recreate
49
+ fs.copyFileSync(MCP_JSON, MCP_JSON + ".bak");
50
+ config = { mcpServers: {} };
51
+ }
52
+ }
53
+
54
+ let changed = false;
55
+
56
+ // Add multiagents MCP server
57
+ if (!config.mcpServers["multiagents"]) {
58
+ config.mcpServers["multiagents"] = {
59
+ command: BUN,
60
+ args: [SERVER_PATH],
61
+ };
62
+ changed = true;
63
+ }
64
+
65
+ // Add orchestrator MCP server
66
+ if (!config.mcpServers["multiagents-orch"]) {
67
+ config.mcpServers["multiagents-orch"] = {
68
+ command: BUN,
69
+ args: [ORCH_PATH],
70
+ };
71
+ changed = true;
72
+ }
73
+
74
+ if (changed) {
75
+ fs.writeFileSync(MCP_JSON, JSON.stringify(config, null, 2) + "\n");
76
+ console.log("[multiagents] MCP servers configured in ~/.claude/.mcp.json");
77
+ console.log(" Restart Claude Code to pick up the new tools.");
78
+ } else {
79
+ console.log("[multiagents] MCP servers already configured.");
80
+ }
81
+ } catch (e) {
82
+ // Postinstall should never fail the install
83
+ console.error(`[multiagents] postinstall warning: ${e instanceof Error ? e.message : String(e)}`);
84
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * SemVer version bumper.
4
+ *
5
+ * Usage:
6
+ * bun run scripts/version.ts patch # 0.1.0 → 0.1.1
7
+ * bun run scripts/version.ts minor # 0.1.0 → 0.2.0
8
+ * bun run scripts/version.ts major # 0.1.0 → 1.0.0
9
+ *
10
+ * What it does:
11
+ * 1. Bumps version in package.json
12
+ * 2. Stages package.json
13
+ * 3. Creates a git commit: "release: vX.Y.Z"
14
+ * 4. Creates a git tag: vX.Y.Z
15
+ */
16
+
17
+ import * as fs from "node:fs";
18
+ import * as path from "node:path";
19
+
20
+ const ROOT = path.resolve(import.meta.dir, "..");
21
+ const PKG_PATH = path.join(ROOT, "package.json");
22
+
23
+ type BumpType = "patch" | "minor" | "major";
24
+
25
+ const bump = process.argv[2] as BumpType;
26
+ if (!["patch", "minor", "major"].includes(bump)) {
27
+ console.error("Usage: bun run scripts/version.ts <patch|minor|major>");
28
+ process.exit(1);
29
+ }
30
+
31
+ // Read current version
32
+ const pkg = JSON.parse(fs.readFileSync(PKG_PATH, "utf-8"));
33
+ const [major, minor, patch] = pkg.version.split(".").map(Number);
34
+
35
+ // Compute new version
36
+ let newVersion: string;
37
+ switch (bump) {
38
+ case "major":
39
+ newVersion = `${major + 1}.0.0`;
40
+ break;
41
+ case "minor":
42
+ newVersion = `${major}.${minor + 1}.0`;
43
+ break;
44
+ case "patch":
45
+ newVersion = `${major}.${minor}.${patch + 1}`;
46
+ break;
47
+ }
48
+
49
+ // Write
50
+ pkg.version = newVersion;
51
+ fs.writeFileSync(PKG_PATH, JSON.stringify(pkg, null, 2) + "\n");
52
+
53
+ console.log(`${pkg.version.replace(newVersion, `${major}.${minor}.${patch}`)} → ${newVersion}`);
54
+
55
+ // Git commit + tag
56
+ const tag = `v${newVersion}`;
57
+ Bun.spawnSync(["git", "add", "package.json"], { cwd: ROOT });
58
+ Bun.spawnSync(["git", "commit", "-m", `release: ${tag}`], { cwd: ROOT });
59
+ Bun.spawnSync(["git", "tag", "-a", tag, "-m", `Release ${tag}`], { cwd: ROOT });
60
+
61
+ console.log(`Committed and tagged: ${tag}`);
62
+ console.log(`To publish: git push && git push --tags && bun publish`);
package/server.ts ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * multiagents MCP server — thin dispatcher
4
+ *
5
+ * Parses --agent-type from argv and delegates to the appropriate adapter.
6
+ *
7
+ * Usage:
8
+ * bun server.ts # defaults to claude
9
+ * bun server.ts --agent-type codex
10
+ * bun server.ts --agent-type gemini
11
+ */
12
+
13
+ import type { AgentType } from "./shared/types.ts";
14
+
15
+ const typeFlag = process.argv.indexOf("--agent-type");
16
+ const agentType: AgentType =
17
+ typeFlag !== -1 && process.argv[typeFlag + 1]
18
+ ? (process.argv[typeFlag + 1] as AgentType)
19
+ : "claude";
20
+
21
+ async function main() {
22
+ switch (agentType) {
23
+ case "claude": {
24
+ const { ClaudeAdapter } = await import("./adapters/claude-adapter.ts");
25
+ await new ClaudeAdapter().start();
26
+ break;
27
+ }
28
+ case "codex": {
29
+ const { CodexAdapter } = await import("./adapters/codex-adapter.ts");
30
+ await new CodexAdapter().start();
31
+ break;
32
+ }
33
+ case "gemini": {
34
+ const { GeminiAdapter } = await import("./adapters/gemini-adapter.ts");
35
+ await new GeminiAdapter().start();
36
+ break;
37
+ }
38
+ default: {
39
+ // For 'custom' or unknown types, fall back to Claude adapter behavior
40
+ const { ClaudeAdapter } = await import("./adapters/claude-adapter.ts");
41
+ await new ClaudeAdapter().start();
42
+ break;
43
+ }
44
+ }
45
+ }
46
+
47
+ main().catch((e) => {
48
+ console.error(
49
+ `[multiagents] Fatal: ${e instanceof Error ? e.message : String(e)}`,
50
+ );
51
+ process.exit(1);
52
+ });