multiclaw 0.3.0 → 0.3.2

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/dist/index.js CHANGED
@@ -1,44 +1,422 @@
1
1
  #!/usr/bin/env node
2
+
3
+ // src/index.ts
2
4
  import { Command } from "commander";
3
5
  import { readFileSync } from "fs";
4
- import { dirname, join } from "path";
6
+ import { dirname as dirname2, join as join3 } from "path";
5
7
  import { fileURLToPath } from "url";
6
- import { runCommand } from "./commands/run";
7
- import { initCommand } from "./commands/init";
8
- import { serveCommand } from "./commands/serve";
9
- import { upgradeCommand } from "./commands/upgrade";
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = dirname(__filename);
12
- const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
13
- const program = new Command();
14
- program
15
- .name("multiclaw")
16
- .description("Multi-agent orchestration CLI")
17
- .version(pkg.version, "-v, --version", "Display version number");
18
- program
19
- .command("init")
20
- .description("Scaffold a config file (interactive)")
21
- .action(() => initCommand());
22
- program
23
- .command("run <requirement>")
24
- .description("Run an orchestration")
25
- .option("--config <path>", "Config file path (default: auto-discover ./multiclaw.config.ts)")
26
- .option("--ui", "Start Dashboard and open in browser")
27
- .option("--port <port>", "Dashboard port (requires --ui)", (v) => parseInt(v, 10))
28
- .option("--server-url <url>", "Connect to a running serve process")
29
- .option("--no-leader", "Skip the leader agent and use the default pipeline")
30
- .action((requirement, opts) => runCommand(requirement, opts));
31
- program
32
- .command("serve")
33
- .description("Start a persistent Dashboard server")
34
- .option("--port <port>", "Port", (v) => parseInt(v, 10))
35
- .action((opts) => serveCommand(opts));
36
- program
37
- .command("upgrade")
38
- .description("Upgrade multiclaw to the latest version")
39
- .action(() => upgradeCommand());
40
- program.parseAsync(process.argv).catch((err) => {
41
- console.error(`\x1b[31mError: ${err.message ?? err}\x1b[0m`);
8
+
9
+ // src/commands/run.ts
10
+ import * as path3 from "path";
11
+ import { Orchestrator as OrchestratorImpl, runtimeRegistry } from "@multiclawcli/core";
12
+
13
+ // src/loader/loadConfig.ts
14
+ import * as path from "path";
15
+ import * as fs from "fs";
16
+ var DEFAULT_CONFIG_NAMES = ["multiclaw.config.ts", "multiclaw.config.js"];
17
+ function findConfig(configPath) {
18
+ if (configPath) {
19
+ const abs = path.resolve(process.cwd(), configPath);
20
+ if (!fs.existsSync(abs)) throw new Error(`Config file not found: ${abs}`);
21
+ return abs;
22
+ }
23
+ for (const name of DEFAULT_CONFIG_NAMES) {
24
+ const abs = path.resolve(process.cwd(), name);
25
+ if (fs.existsSync(abs)) return abs;
26
+ }
27
+ throw new Error(`No config file found. Create multiclaw.config.ts in the current directory or use --config to specify a path.`);
28
+ }
29
+ async function loadConfig(configPath, requirement) {
30
+ const absPath = findConfig(configPath);
31
+ const { register } = await import("tsx/esm/api");
32
+ const unregister = register();
33
+ let mod;
34
+ try {
35
+ mod = await import(absPath);
36
+ } finally {
37
+ unregister();
38
+ }
39
+ const config = mod.default ?? mod.config;
40
+ if (!config || !Array.isArray(config.agents)) {
41
+ throw new Error(`Invalid config format \u2014 expected: export default defineConfig({...})`);
42
+ }
43
+ const configDir = path.dirname(absPath);
44
+ config.workDir = path.resolve(configDir, config.workDir);
45
+ if (requirement !== void 0) {
46
+ config.context = { ...config.context, requirement };
47
+ }
48
+ return config;
49
+ }
50
+
51
+ // src/reporters/ConsoleReporter.ts
52
+ var isFancy = !process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY === true;
53
+ var S = isFancy ? { ok: "\u2713", fail: "\u2717", run: "\u203A", retry: "\u21BB", skip: "\u2298", warn: "\u26A0", bullet: "\xB7" } : { ok: "+", fail: "x", run: ">", retry: "~", skip: "-", warn: "!", bullet: "*" };
54
+ var C = isFancy ? {
55
+ reset: "\x1B[0m",
56
+ bold: "\x1B[1m",
57
+ dim: "\x1B[2m",
58
+ cyan: "\x1B[36m",
59
+ green: "\x1B[32m",
60
+ yellow: "\x1B[33m",
61
+ red: "\x1B[31m",
62
+ purple: "\x1B[35m"
63
+ } : {
64
+ reset: "",
65
+ bold: "",
66
+ dim: "",
67
+ cyan: "",
68
+ green: "",
69
+ yellow: "",
70
+ red: "",
71
+ purple: ""
72
+ };
73
+ var ConsoleReporter = class {
74
+ attach(eventBus) {
75
+ return eventBus.subscribe((e) => this.handle(e));
76
+ }
77
+ handle(e) {
78
+ switch (e.type) {
79
+ case "orchestration:start": {
80
+ const line = (isFancy ? "\u2550" : "=").repeat(50);
81
+ const edge = isFancy ? ["\u2554", "\u2551", "\u255A", "\u2557", "\u255D"] : ["+", "|", "+", "+", "+"];
82
+ console.log(`
83
+ ${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
84
+ console.log(`${edge[1]} ${S.run} ${e.payload.name.padEnd(46)}${edge[1]}`);
85
+ console.log(`${edge[2]}${line}${edge[4]}${C.reset}`);
86
+ console.log(`${C.cyan}${S.bullet} Agents: ${e.payload.totalAgents} Stages: ${e.payload.stages.length}${C.reset}
87
+ `);
88
+ break;
89
+ }
90
+ case "stage:start": {
91
+ const dash = (isFancy ? "\u2500" : "-").repeat(16);
92
+ console.log(`${C.cyan}${dash} Stage ${e.payload.stageIndex + 1} ${dash}${C.reset}`);
93
+ break;
94
+ }
95
+ case "agent:start": {
96
+ console.log(`${C.purple}${S.run} [${e.payload.agentName}] starting...${C.reset}`);
97
+ break;
98
+ }
99
+ case "agent:output": {
100
+ process.stdout.write(` ${e.payload.chunk}`);
101
+ break;
102
+ }
103
+ case "agent:complete": {
104
+ const d = (e.payload.duration / 1e3).toFixed(1);
105
+ console.log(`${C.green}${S.ok} ${e.payload.agentName} done (${d}s)${C.reset}`);
106
+ break;
107
+ }
108
+ case "agent:failed": {
109
+ console.log(`${C.red}${S.fail} ${e.payload.agentName} failed: ${e.payload.error}${C.reset}`);
110
+ break;
111
+ }
112
+ case "agent:skipped": {
113
+ console.log(`${C.yellow}${S.skip} ${e.payload.agentName} skipped${C.reset}`);
114
+ break;
115
+ }
116
+ case "agent:retrying": {
117
+ console.log(`${C.yellow}${S.retry} ${e.payload.agentName} retry ${e.payload.attempt}/${e.payload.maxAttempts}: ${e.payload.error}${C.reset}`);
118
+ break;
119
+ }
120
+ case "orchestration:complete": {
121
+ const { success, totalDuration, agentResults } = e.payload;
122
+ const line = (isFancy ? "\u2550" : "=").repeat(50);
123
+ const edge = isFancy ? ["\u2554", "\u2551", "\u255A", "\u2557", "\u255D"] : ["+", "|", "+", "+", "+"];
124
+ const title = success ? `${S.ok} All done` : `${S.warn} Done (with failures)`;
125
+ console.log(`
126
+ ${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
127
+ console.log(`${edge[1]} ${title.padEnd(48)}${edge[1]}`);
128
+ console.log(`${edge[2]}${line}${edge[4]}${C.reset}`);
129
+ const nameWidth = Math.max(8, ...agentResults.map((r) => r.agentName.length));
130
+ const totalStr = `${(totalDuration / 1e3).toFixed(1)}s`;
131
+ console.log(` ~ ${"Total".padEnd(nameWidth)} ${totalStr.padStart(7)}`);
132
+ for (const r of agentResults) {
133
+ const icon = r.status === "success" ? S.ok : S.fail;
134
+ const color = r.status === "success" ? C.green : C.red;
135
+ const dur = `${(r.duration / 1e3).toFixed(1)}s`;
136
+ console.log(` ${color}${icon}${C.reset} ${r.agentName.padEnd(nameWidth)} ${dur.padStart(7)}`);
137
+ }
138
+ console.log();
139
+ break;
140
+ }
141
+ }
142
+ }
143
+ };
144
+
145
+ // src/reporters/FileLogWriter.ts
146
+ import * as fs2 from "fs";
147
+ import * as path2 from "path";
148
+ var FileLogWriter = class {
149
+ constructor(logDir) {
150
+ this.logDir = logDir;
151
+ this.streams = /* @__PURE__ */ new Map();
152
+ fs2.mkdirSync(logDir, { recursive: true });
153
+ }
154
+ attach(eventBus) {
155
+ return eventBus.subscribe((e) => this.handle(e));
156
+ }
157
+ handle(e) {
158
+ if (e.type === "agent:output") {
159
+ this.getStream(e.payload.agentId).write(e.payload.chunk);
160
+ }
161
+ if (e.type === "orchestration:complete") {
162
+ fs2.writeFileSync(
163
+ path2.join(this.logDir, "report.json"),
164
+ JSON.stringify(e.payload, null, 2)
165
+ );
166
+ this.closeAll();
167
+ }
168
+ }
169
+ getStream(agentId) {
170
+ if (!this.streams.has(agentId)) {
171
+ this.streams.set(
172
+ agentId,
173
+ fs2.createWriteStream(path2.join(this.logDir, `${agentId}.log`))
174
+ );
175
+ }
176
+ return this.streams.get(agentId);
177
+ }
178
+ closeAll() {
179
+ for (const s of this.streams.values()) s.end();
180
+ this.streams.clear();
181
+ }
182
+ };
183
+
184
+ // src/commands/run.ts
185
+ async function runCommand(requirement, options) {
186
+ const config = await loadConfig(options.config, requirement);
187
+ if (options.noLeader) {
188
+ config.useLeader = false;
189
+ }
190
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
191
+ config.workDir = path3.join(config.workDir, ".multiclaw", "runs", `run-${ts}`);
192
+ const runtimeNames = new Set(config.agents.map((a) => a.runtime ?? "claude"));
193
+ for (const name of runtimeNames) {
194
+ let rt;
195
+ try {
196
+ rt = runtimeRegistry.get(name);
197
+ } catch {
198
+ console.error(`\x1B[31mError: Unknown runtime "${name}". Supported: claude\x1B[0m`);
199
+ process.exit(1);
200
+ }
201
+ if (!await rt.checkAvailable()) {
202
+ console.error(`\x1B[31mError: Runtime "${name}" is not available. Make sure the CLI is installed.\x1B[0m`);
203
+ process.exit(1);
204
+ }
205
+ }
206
+ const orchestrator = new OrchestratorImpl(config);
207
+ new ConsoleReporter().attach(orchestrator.eventBus);
208
+ const logDir = config.logDir ?? path3.join(config.workDir, "logs");
209
+ new FileLogWriter(logDir).attach(orchestrator.eventBus);
210
+ console.log(`\x1B[2mLogs: ${logDir}\x1B[0m`);
211
+ if (options.ui && options.serverUrl) {
212
+ console.warn("\x1B[33mWarning: --ui and --server-url cannot be used together. Using --ui.\x1B[0m");
213
+ }
214
+ if (options.ui) {
215
+ await attachDashboard(orchestrator, config, options);
216
+ } else if (options.serverUrl) {
217
+ await attachRemoteServer(orchestrator, options.serverUrl);
218
+ }
219
+ const result = await orchestrator.run();
220
+ process.exit(result.success ? 0 : 1);
221
+ }
222
+ async function attachDashboard(orchestrator, config, options) {
223
+ const { DashboardServer } = await import("@multiclawcli/dashboard/server");
224
+ const port = options.port ?? config.dashboard?.port ?? 3210;
225
+ const server = new DashboardServer({ port });
226
+ await server.start();
227
+ server.bindEventBus(orchestrator.eventBus);
228
+ const url = `http://localhost:${port}`;
229
+ console.log(`\x1B[36mDashboard: ${url}\x1B[0m`);
230
+ if (config.dashboard?.autoOpen ?? true) {
231
+ const { default: open } = await import("open");
232
+ await open(url);
233
+ }
234
+ }
235
+ async function attachRemoteServer(orchestrator, serverUrl) {
236
+ const { WebSocket } = await import("ws");
237
+ const wsUrl = serverUrl.replace(/^https?/, (p) => p === "https" ? "wss" : "ws");
238
+ const ws = new WebSocket(wsUrl);
239
+ await new Promise((resolve3, reject) => {
240
+ const timer = setTimeout(() => reject(new Error(`Connection to serve timed out: ${serverUrl}`)), 5e3);
241
+ ws.on("open", () => {
242
+ clearTimeout(timer);
243
+ resolve3();
244
+ });
245
+ ws.on("error", (err) => {
246
+ clearTimeout(timer);
247
+ reject(err);
248
+ });
249
+ });
250
+ console.log(`\x1B[36mConnected to Dashboard server: ${serverUrl}\x1B[0m`);
251
+ orchestrator.eventBus.subscribe((e) => {
252
+ if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(e));
253
+ });
254
+ }
255
+
256
+ // src/commands/init.ts
257
+ import * as fs3 from "fs";
258
+ import * as path4 from "path";
259
+ import { execSync } from "child_process";
260
+ import { select, input } from "@inquirer/prompts";
261
+ var AGENT_BLOCKS = {
262
+ simple: ` agents.architect(),
263
+ agents.archDesignReviewer({ dependsOn: ["architect"] }),
264
+ agents.backendDeveloper({ dependsOn: ["arch-design-reviewer"] }),
265
+ agents.codeReviewer({ dependsOn: ["backend-developer"] }),`,
266
+ backend: ` agents.productManager(),
267
+ agents.architect({ dependsOn: ["product-manager"] }),
268
+ agents.archDesignReviewer({ dependsOn: ["architect"] }),
269
+ agents.backendDeveloper({ dependsOn: ["arch-design-reviewer"] }),
270
+ agents.tester({ dependsOn: ["backend-developer"] }),
271
+ agents.codeReviewer({ dependsOn: ["tester"] }),
272
+ agents.devops({ dependsOn: ["tester"] }),`,
273
+ fullstack: ` agents.productManager(),
274
+ agents.architect({ dependsOn: ["product-manager"] }),
275
+ agents.archDesignReviewer({ dependsOn: ["architect"] }),
276
+ agents.backendDeveloper({ dependsOn: ["arch-design-reviewer"] }),
277
+ agents.uiDesigner({ dependsOn: ["arch-design-reviewer"] }),
278
+ agents.uiDesignReviewer({ dependsOn: ["ui-designer"] }),
279
+ agents.frontendDeveloper({ dependsOn: ["ui-design-reviewer"] }),
280
+ agents.codeReviewer({ dependsOn: ["backend-developer", "frontend-developer"] }),
281
+ agents.devops({ dependsOn: ["backend-developer", "frontend-developer"] }),`
282
+ };
283
+ var PRESET_DESCRIPTIONS = {
284
+ simple: "Architect \u2192 Arch Design Reviewer \u2192 Backend Developer \u2192 Code Reviewer (4 agents)",
285
+ backend: "Product Manager \u2192 Architect \u2192 Arch Design Reviewer \u2192 Backend Developer \u2192 Tester \u2192 Code Reviewer + DevOps (7 agents)",
286
+ fullstack: "Product Manager \u2192 Architect \u2192 Arch Design Reviewer \u2192 Backend Developer + UI Designer \u2192 UI Design Reviewer \u2192 Frontend Developer \u2192 Code Reviewer + DevOps (9 agents)"
287
+ };
288
+ var RUNTIME_INSTALL = {
289
+ claude: "npm install -g @anthropic-ai/claude-code",
290
+ codex: "npm install -g @openai/codex",
291
+ opencode: "npm install -g opencode",
292
+ cursor: "curl https://cursor.com/install | bash"
293
+ };
294
+ function generateConfig(opts) {
295
+ return `import { defineConfig, agents } from "multiclaw"
296
+
297
+ export default defineConfig({
298
+ name: "${opts.projectName}",
299
+ workDir: "${opts.workDir}",
300
+ context: {
301
+ projectName: "${opts.projectName}",
302
+ },
303
+ dashboard: { port: 3210, autoOpen: true },
304
+ agents: [
305
+ ${AGENT_BLOCKS[opts.preset]}
306
+ ],
307
+ })
308
+ `;
309
+ }
310
+ async function initCommand() {
311
+ const configPath = path4.resolve(process.cwd(), "multiclaw.config.ts");
312
+ if (fs3.existsSync(configPath)) {
313
+ console.error("\x1B[31mError: multiclaw.config.ts already exists\x1B[0m");
314
+ process.exit(1);
315
+ }
316
+ const defaultProjectName = path4.basename(process.cwd());
317
+ const projectName = await input({
318
+ message: "Project name:",
319
+ default: defaultProjectName
320
+ });
321
+ const preset = await select({
322
+ message: "Select a preset:",
323
+ choices: [
324
+ { value: "simple", name: `simple \u2014 ${PRESET_DESCRIPTIONS.simple}` },
325
+ { value: "backend", name: `backend \u2014 ${PRESET_DESCRIPTIONS.backend}` },
326
+ { value: "fullstack", name: `fullstack \u2014 ${PRESET_DESCRIPTIONS.fullstack}` }
327
+ ]
328
+ });
329
+ const workDir = await input({
330
+ message: "Work directory (where agents read and write files):",
331
+ default: "."
332
+ });
333
+ try {
334
+ fs3.writeFileSync(configPath, generateConfig({ preset, workDir, projectName }));
335
+ if (workDir !== ".") {
336
+ fs3.mkdirSync(path4.resolve(process.cwd(), workDir), { recursive: true });
337
+ }
338
+ } catch (err) {
339
+ console.error(`\x1B[31mError: Failed to create files: ${err.message}\x1B[0m`);
340
+ process.exit(1);
341
+ }
342
+ console.log("");
343
+ console.log(`\x1B[32m\u2713\x1B[0m Created multiclaw.config.ts`);
344
+ console.log(` preset: ${preset} (${PRESET_DESCRIPTIONS[preset]})`);
345
+ console.log(` workDir: ${workDir}`);
346
+ console.log("");
347
+ console.log(`> Run: multiclaw run "describe your requirement" --ui`);
348
+ checkRuntime();
349
+ }
350
+ function checkRuntime() {
351
+ const runtimes = ["claude", "codex", "opencode"];
352
+ const available = runtimes.filter((r) => {
353
+ try {
354
+ execSync(`${r} --version`, { stdio: "ignore" });
355
+ return true;
356
+ } catch {
357
+ return false;
358
+ }
359
+ });
360
+ if (available.length === 0) {
361
+ console.log("");
362
+ console.log("\x1B[33mWarning: No AI runtime detected. Install at least one:\x1B[0m");
363
+ for (const [name, cmd] of Object.entries(RUNTIME_INSTALL)) {
364
+ console.log(` \x1B[2m${name.padEnd(12)} ${cmd}\x1B[0m`);
365
+ }
366
+ }
367
+ }
368
+
369
+ // src/commands/serve.ts
370
+ async function serveCommand(options) {
371
+ const { DashboardServer } = await import("@multiclawcli/dashboard/server");
372
+ const port = options.port ?? 3210;
373
+ const server = new DashboardServer({ port, acceptClientEvents: true });
374
+ await server.start();
375
+ console.log(`\x1B[36mDashboard running at: http://localhost:${port}\x1B[0m`);
376
+ console.log(` In another terminal run: multiclaw run "<requirement>" --server-url http://localhost:${port}`);
377
+ }
378
+
379
+ // src/commands/upgrade.ts
380
+ import { execSync as execSync2 } from "child_process";
381
+ import { createRequire } from "module";
382
+ var require2 = createRequire(import.meta.url);
383
+ async function upgradeCommand() {
384
+ const { version: currentVersion } = require2("../package.json");
385
+ console.log(`Current version: v${currentVersion}`);
386
+ console.log("Checking for updates...");
387
+ let latestVersion;
388
+ try {
389
+ latestVersion = execSync2("npm show multiclaw version", { encoding: "utf8" }).trim();
390
+ } catch {
391
+ console.error("\x1B[31mError: Failed to fetch latest version from npm registry\x1B[0m");
42
392
  process.exit(1);
393
+ return;
394
+ }
395
+ if (latestVersion === currentVersion) {
396
+ console.log(`\x1B[32m\u2713\x1B[0m Already on the latest version (v${currentVersion})`);
397
+ return;
398
+ }
399
+ console.log(`Upgrading multiclaw v${currentVersion} \u2192 v${latestVersion}...`);
400
+ try {
401
+ execSync2("npm install -g multiclaw@latest", { stdio: "inherit" });
402
+ console.log(`\x1B[32m\u2713\x1B[0m Successfully upgraded to v${latestVersion}`);
403
+ } catch {
404
+ console.error("\x1B[31mError: Upgrade failed. Try running manually: npm install -g multiclaw@latest\x1B[0m");
405
+ process.exit(1);
406
+ }
407
+ }
408
+
409
+ // src/index.ts
410
+ var __filename2 = fileURLToPath(import.meta.url);
411
+ var __dirname2 = dirname2(__filename2);
412
+ var pkg = JSON.parse(readFileSync(join3(__dirname2, "../package.json"), "utf-8"));
413
+ var program = new Command();
414
+ program.name("multiclaw").description("Multi-agent orchestration CLI").version(pkg.version, "-v, --version", "Display version number");
415
+ program.command("init").description("Scaffold a config file (interactive)").action(() => initCommand());
416
+ program.command("run <requirement>").description("Run an orchestration").option("--config <path>", "Config file path (default: auto-discover ./multiclaw.config.ts)").option("--ui", "Start Dashboard and open in browser").option("--port <port>", "Dashboard port (requires --ui)", (v) => parseInt(v, 10)).option("--server-url <url>", "Connect to a running serve process").option("--no-leader", "Skip the leader agent and use the default pipeline").action((requirement, opts) => runCommand(requirement, opts));
417
+ program.command("serve").description("Start a persistent Dashboard server").option("--port <port>", "Port", (v) => parseInt(v, 10)).action((opts) => serveCommand(opts));
418
+ program.command("upgrade").description("Upgrade multiclaw to the latest version").action(() => upgradeCommand());
419
+ program.parseAsync(process.argv).catch((err) => {
420
+ console.error(`\x1B[31mError: ${err.message ?? err}\x1B[0m`);
421
+ process.exit(1);
43
422
  });
44
- //# sourceMappingURL=index.js.map
package/dist/lib.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { AgentDefinition, MultiClawConfig, agents, defineConfig } from '@multiclawcli/core';
package/dist/lib.js CHANGED
@@ -1,2 +1,6 @@
1
- export { defineConfig, agents } from "@multiclawcli/core";
2
- //# sourceMappingURL=lib.js.map
1
+ // src/lib.ts
2
+ import { defineConfig, agents } from "@multiclawcli/core";
3
+ export {
4
+ agents,
5
+ defineConfig
6
+ };
package/package.json CHANGED
@@ -1,10 +1,14 @@
1
1
  {
2
2
  "name": "multiclaw",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "multiclaw": "dist/index.js"
7
7
  },
8
+ "files": [
9
+ "dist",
10
+ "package.json"
11
+ ],
8
12
  "main": "dist/lib.js",
9
13
  "types": "dist/lib.d.ts",
10
14
  "exports": {
@@ -24,8 +28,8 @@
24
28
  },
25
29
  "dependencies": {
26
30
  "@inquirer/prompts": "^8.7.3",
27
- "@multiclawcli/core": "0.3.0",
28
- "@multiclawcli/dashboard": "0.3.0",
31
+ "@multiclawcli/core": "0.3.2",
32
+ "@multiclawcli/dashboard": "0.3.2",
29
33
  "commander": "^12.0.0",
30
34
  "open": "^10.0.0",
31
35
  "tsx": "^4.7.0",
@@ -37,7 +41,7 @@
37
41
  "typescript": "^5.4.0"
38
42
  },
39
43
  "scripts": {
40
- "build": "tsc",
44
+ "build": "tsup",
41
45
  "dev": "tsx src/index.ts"
42
46
  }
43
47
  }
@@ -1,128 +0,0 @@
1
- import * as fs from "fs"
2
- import * as path from "path"
3
- import { execSync } from "child_process"
4
- import { select, input } from "@inquirer/prompts"
5
-
6
- // ─── Agent Block Definitions ──────────────────────────────────────────────────
7
-
8
- const AGENT_BLOCKS: Record<string, string> = {
9
- simple: ` agents.architect(),
10
- agents.archDesignReviewer({ dependsOn: ["architect"] }),
11
- agents.backendDeveloper({ dependsOn: ["arch-design-reviewer"] }),
12
- agents.codeReviewer({ dependsOn: ["backend-developer"] }),`,
13
-
14
- backend: ` agents.productManager(),
15
- agents.architect({ dependsOn: ["product-manager"] }),
16
- agents.archDesignReviewer({ dependsOn: ["architect"] }),
17
- agents.backendDeveloper({ dependsOn: ["arch-design-reviewer"] }),
18
- agents.tester({ dependsOn: ["backend-developer"] }),
19
- agents.codeReviewer({ dependsOn: ["tester"] }),
20
- agents.devops({ dependsOn: ["tester"] }),`,
21
-
22
- fullstack: ` agents.productManager(),
23
- agents.architect({ dependsOn: ["product-manager"] }),
24
- agents.archDesignReviewer({ dependsOn: ["architect"] }),
25
- agents.backendDeveloper({ dependsOn: ["arch-design-reviewer"] }),
26
- agents.uiDesigner({ dependsOn: ["arch-design-reviewer"] }),
27
- agents.uiDesignReviewer({ dependsOn: ["ui-designer"] }),
28
- agents.frontendDeveloper({ dependsOn: ["ui-design-reviewer"] }),
29
- agents.codeReviewer({ dependsOn: ["backend-developer", "frontend-developer"] }),
30
- agents.devops({ dependsOn: ["backend-developer", "frontend-developer"] }),`,
31
- }
32
-
33
- const PRESET_DESCRIPTIONS: Record<string, string> = {
34
- simple: "Architect → Arch Design Reviewer → Backend Developer → Code Reviewer (4 agents)",
35
- backend: "Product Manager → Architect → Arch Design Reviewer → Backend Developer → Tester → Code Reviewer + DevOps (7 agents)",
36
- fullstack: "Product Manager → Architect → Arch Design Reviewer → Backend Developer + UI Designer → UI Design Reviewer → Frontend Developer → Code Reviewer + DevOps (9 agents)",
37
- }
38
-
39
- const RUNTIME_INSTALL: Record<string, string> = {
40
- claude: "npm install -g @anthropic-ai/claude-code",
41
- codex: "npm install -g @openai/codex",
42
- opencode: "npm install -g opencode",
43
- cursor: "curl https://cursor.com/install | bash",
44
- }
45
-
46
- // ─── Config Generator ─────────────────────────────────────────────────────────
47
-
48
- function generateConfig(opts: { preset: string; workDir: string; projectName: string }): string {
49
- return `import { defineConfig, agents } from "multiclaw"
50
-
51
- export default defineConfig({
52
- name: "${opts.projectName}",
53
- workDir: "${opts.workDir}",
54
- context: {
55
- projectName: "${opts.projectName}",
56
- },
57
- dashboard: { port: 3210, autoOpen: true },
58
- agents: [
59
- ${AGENT_BLOCKS[opts.preset]}
60
- ],
61
- })
62
- `
63
- }
64
-
65
- // ─── Command ──────────────────────────────────────────────────────────────────
66
-
67
- export async function initCommand(): Promise<void> {
68
- const configPath = path.resolve(process.cwd(), "multiclaw.config.ts")
69
- if (fs.existsSync(configPath)) {
70
- console.error("\x1b[31mError: multiclaw.config.ts already exists\x1b[0m")
71
- process.exit(1)
72
- }
73
-
74
- const defaultProjectName = path.basename(process.cwd())
75
-
76
- const projectName = await input({
77
- message: "Project name:",
78
- default: defaultProjectName,
79
- })
80
-
81
- const preset = await select({
82
- message: "Select a preset:",
83
- choices: [
84
- { value: "simple", name: `simple — ${PRESET_DESCRIPTIONS.simple}` },
85
- { value: "backend", name: `backend — ${PRESET_DESCRIPTIONS.backend}` },
86
- { value: "fullstack", name: `fullstack — ${PRESET_DESCRIPTIONS.fullstack}` },
87
- ],
88
- })
89
-
90
- const workDir = await input({
91
- message: "Work directory (where agents read and write files):",
92
- default: ".",
93
- })
94
-
95
- try {
96
- fs.writeFileSync(configPath, generateConfig({ preset, workDir, projectName }))
97
- if (workDir !== ".") {
98
- fs.mkdirSync(path.resolve(process.cwd(), workDir), { recursive: true })
99
- }
100
- } catch (err) {
101
- console.error(`\x1b[31mError: Failed to create files: ${(err as Error).message}\x1b[0m`)
102
- process.exit(1)
103
- }
104
-
105
- console.log("")
106
- console.log(`\x1b[32m✓\x1b[0m Created multiclaw.config.ts`)
107
- console.log(` preset: ${preset} (${PRESET_DESCRIPTIONS[preset]})`)
108
- console.log(` workDir: ${workDir}`)
109
- console.log("")
110
- console.log(`> Run: multiclaw run "describe your requirement" --ui`)
111
-
112
- checkRuntime()
113
- }
114
-
115
- function checkRuntime(): void {
116
- const runtimes = ["claude", "codex", "opencode"]
117
- const available = runtimes.filter((r) => {
118
- try { execSync(`${r} --version`, { stdio: "ignore" }); return true }
119
- catch { return false }
120
- })
121
- if (available.length === 0) {
122
- console.log("")
123
- console.log("\x1b[33mWarning: No AI runtime detected. Install at least one:\x1b[0m")
124
- for (const [name, cmd] of Object.entries(RUNTIME_INSTALL)) {
125
- console.log(` \x1b[2m${name.padEnd(12)} ${cmd}\x1b[0m`)
126
- }
127
- }
128
- }
@@ -1,97 +0,0 @@
1
- import * as path from "path"
2
- import type { Orchestrator } from "@multiclawcli/core"
3
- import { Orchestrator as OrchestratorImpl, runtimeRegistry } from "@multiclawcli/core"
4
- import { loadConfig } from "../loader/loadConfig"
5
- import { ConsoleReporter } from "../reporters/ConsoleReporter"
6
- import { FileLogWriter } from "../reporters/FileLogWriter"
7
-
8
- export interface RunOptions {
9
- config?: string
10
- ui?: boolean
11
- serverUrl?: string
12
- port?: number
13
- noLeader?: boolean
14
- }
15
-
16
- export async function runCommand(requirement: string, options: RunOptions): Promise<void> {
17
- const config = await loadConfig(options.config, requirement)
18
-
19
- if (options.noLeader) {
20
- config.useLeader = false
21
- }
22
-
23
- const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19)
24
- config.workDir = path.join(config.workDir, ".multiclaw", "runs", `run-${ts}`)
25
-
26
- const runtimeNames = new Set(config.agents.map((a) => a.runtime ?? "claude"))
27
- for (const name of runtimeNames) {
28
- let rt
29
- try { rt = runtimeRegistry.get(name) } catch {
30
- console.error(`\x1b[31mError: Unknown runtime "${name}". Supported: claude\x1b[0m`)
31
- process.exit(1)
32
- }
33
- if (!(await rt.checkAvailable())) {
34
- console.error(`\x1b[31mError: Runtime "${name}" is not available. Make sure the CLI is installed.\x1b[0m`)
35
- process.exit(1)
36
- }
37
- }
38
-
39
- const orchestrator = new OrchestratorImpl(config)
40
-
41
- new ConsoleReporter().attach(orchestrator.eventBus)
42
-
43
- const logDir = config.logDir ?? path.join(config.workDir, "logs")
44
- new FileLogWriter(logDir).attach(orchestrator.eventBus)
45
- console.log(`\x1b[2mLogs: ${logDir}\x1b[0m`)
46
-
47
- if (options.ui && options.serverUrl) {
48
- console.warn("\x1b[33mWarning: --ui and --server-url cannot be used together. Using --ui.\x1b[0m")
49
- }
50
-
51
- if (options.ui) {
52
- await attachDashboard(orchestrator, config, options)
53
- } else if (options.serverUrl) {
54
- await attachRemoteServer(orchestrator, options.serverUrl)
55
- }
56
-
57
- const result = await orchestrator.run()
58
- process.exit(result.success ? 0 : 1)
59
- }
60
-
61
- async function attachDashboard(
62
- orchestrator: Orchestrator,
63
- config: Awaited<ReturnType<typeof loadConfig>>,
64
- options: RunOptions
65
- ): Promise<void> {
66
- const { DashboardServer } = await import("@multiclawcli/dashboard/server")
67
- const port = options.port ?? config.dashboard?.port ?? 3210
68
- const server = new DashboardServer({ port })
69
- await server.start()
70
- server.bindEventBus(orchestrator.eventBus)
71
-
72
- const url = `http://localhost:${port}`
73
- console.log(`\x1b[36mDashboard: ${url}\x1b[0m`)
74
-
75
- if (config.dashboard?.autoOpen ?? true) {
76
- const { default: open } = await import("open")
77
- await open(url)
78
- }
79
- }
80
-
81
- async function attachRemoteServer(
82
- orchestrator: Orchestrator,
83
- serverUrl: string
84
- ): Promise<void> {
85
- const { WebSocket } = await import("ws")
86
- const wsUrl = serverUrl.replace(/^https?/, (p) => p === "https" ? "wss" : "ws")
87
- const ws = new WebSocket(wsUrl)
88
- await new Promise<void>((resolve, reject) => {
89
- const timer = setTimeout(() => reject(new Error(`Connection to serve timed out: ${serverUrl}`)), 5000)
90
- ws.on("open", () => { clearTimeout(timer); resolve() })
91
- ws.on("error", (err) => { clearTimeout(timer); reject(err) })
92
- })
93
- console.log(`\x1b[36mConnected to Dashboard server: ${serverUrl}\x1b[0m`)
94
- orchestrator.eventBus.subscribe((e) => {
95
- if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(e))
96
- })
97
- }
@@ -1,12 +0,0 @@
1
- export interface ServeOptions {
2
- port?: number
3
- }
4
-
5
- export async function serveCommand(options: ServeOptions): Promise<void> {
6
- const { DashboardServer } = await import("@multiclawcli/dashboard/server")
7
- const port = options.port ?? 3210
8
- const server = new DashboardServer({ port, acceptClientEvents: true })
9
- await server.start()
10
- console.log(`\x1b[36mDashboard running at: http://localhost:${port}\x1b[0m`)
11
- console.log(` In another terminal run: multiclaw run "<requirement>" --server-url http://localhost:${port}`)
12
- }
@@ -1,35 +0,0 @@
1
- import { execSync } from "child_process"
2
- import { createRequire } from "module"
3
-
4
- const require = createRequire(import.meta.url)
5
-
6
- export async function upgradeCommand(): Promise<void> {
7
- const { version: currentVersion } = require("../../package.json") as { version: string }
8
-
9
- console.log(`Current version: v${currentVersion}`)
10
- console.log("Checking for updates...")
11
-
12
- let latestVersion: string
13
- try {
14
- latestVersion = execSync("npm show multiclaw version", { encoding: "utf8" }).trim()
15
- } catch {
16
- console.error("\x1b[31mError: Failed to fetch latest version from npm registry\x1b[0m")
17
- process.exit(1)
18
- return
19
- }
20
-
21
- if (latestVersion === currentVersion) {
22
- console.log(`\x1b[32m✓\x1b[0m Already on the latest version (v${currentVersion})`)
23
- return
24
- }
25
-
26
- console.log(`Upgrading multiclaw v${currentVersion} → v${latestVersion}...`)
27
-
28
- try {
29
- execSync("npm install -g multiclaw@latest", { stdio: "inherit" })
30
- console.log(`\x1b[32m✓\x1b[0m Successfully upgraded to v${latestVersion}`)
31
- } catch {
32
- console.error("\x1b[31mError: Upgrade failed. Try running manually: npm install -g multiclaw@latest\x1b[0m")
33
- process.exit(1)
34
- }
35
- }
package/src/index.ts DELETED
@@ -1,51 +0,0 @@
1
- #!/usr/bin/env node
2
- import { Command } from "commander"
3
- import { readFileSync } from "fs"
4
- import { dirname, join } from "path"
5
- import { fileURLToPath } from "url"
6
- import { runCommand } from "./commands/run"
7
- import { initCommand } from "./commands/init"
8
- import { serveCommand } from "./commands/serve"
9
- import { upgradeCommand } from "./commands/upgrade"
10
-
11
- const __filename = fileURLToPath(import.meta.url)
12
- const __dirname = dirname(__filename)
13
- const pkg = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"))
14
-
15
- const program = new Command()
16
-
17
- program
18
- .name("multiclaw")
19
- .description("Multi-agent orchestration CLI")
20
- .version(pkg.version, "-v, --version", "Display version number")
21
-
22
- program
23
- .command("init")
24
- .description("Scaffold a config file (interactive)")
25
- .action(() => initCommand())
26
-
27
- program
28
- .command("run <requirement>")
29
- .description("Run an orchestration")
30
- .option("--config <path>", "Config file path (default: auto-discover ./multiclaw.config.ts)")
31
- .option("--ui", "Start Dashboard and open in browser")
32
- .option("--port <port>", "Dashboard port (requires --ui)", (v) => parseInt(v, 10))
33
- .option("--server-url <url>", "Connect to a running serve process")
34
- .option("--no-leader", "Skip the leader agent and use the default pipeline")
35
- .action((requirement, opts) => runCommand(requirement, opts))
36
-
37
- program
38
- .command("serve")
39
- .description("Start a persistent Dashboard server")
40
- .option("--port <port>", "Port", (v) => parseInt(v, 10))
41
- .action((opts) => serveCommand(opts))
42
-
43
- program
44
- .command("upgrade")
45
- .description("Upgrade multiclaw to the latest version")
46
- .action(() => upgradeCommand())
47
-
48
- program.parseAsync(process.argv).catch((err: unknown) => {
49
- console.error(`\x1b[31mError: ${(err as Error).message ?? err}\x1b[0m`)
50
- process.exit(1)
51
- })
package/src/lib.ts DELETED
@@ -1,2 +0,0 @@
1
- export { defineConfig, agents } from "@multiclawcli/core"
2
- export type { MultiClawConfig, AgentDefinition } from "@multiclawcli/core"
@@ -1,47 +0,0 @@
1
- import * as path from "path"
2
- import * as fs from "fs"
3
- import type { MultiClawConfig } from "@multiclawcli/core"
4
-
5
- const DEFAULT_CONFIG_NAMES = ["multiclaw.config.ts", "multiclaw.config.js"]
6
-
7
- function findConfig(configPath?: string): string {
8
- if (configPath) {
9
- const abs = path.resolve(process.cwd(), configPath)
10
- if (!fs.existsSync(abs)) throw new Error(`Config file not found: ${abs}`)
11
- return abs
12
- }
13
- for (const name of DEFAULT_CONFIG_NAMES) {
14
- const abs = path.resolve(process.cwd(), name)
15
- if (fs.existsSync(abs)) return abs
16
- }
17
- throw new Error(`No config file found. Create multiclaw.config.ts in the current directory or use --config to specify a path.`)
18
- }
19
-
20
- export async function loadConfig(configPath?: string, requirement?: string): Promise<MultiClawConfig> {
21
- const absPath = findConfig(configPath)
22
-
23
- // The compiled CLI cannot natively import .ts files — register tsx ESM hook
24
- // so dynamic import can handle TypeScript config files.
25
- const { register } = await import("tsx/esm/api")
26
- const unregister = register()
27
- let mod: Record<string, unknown>
28
- try {
29
- mod = await import(absPath)
30
- } finally {
31
- unregister()
32
- }
33
- const config: MultiClawConfig = (mod.default ?? mod.config) as MultiClawConfig
34
-
35
- if (!config || !Array.isArray(config.agents)) {
36
- throw new Error(`Invalid config format — expected: export default defineConfig({...})`)
37
- }
38
-
39
- const configDir = path.dirname(absPath)
40
- config.workDir = path.resolve(configDir, config.workDir)
41
-
42
- if (requirement !== undefined) {
43
- config.context = { ...config.context, requirement }
44
- }
45
-
46
- return config
47
- }
@@ -1,93 +0,0 @@
1
- import type { EventBus, MultiClawEvent } from "@multiclawcli/core"
2
-
3
- // Degraded to plain ASCII when NO_COLOR is set, TERM=dumb, or stdout is not a TTY (pipe/CI).
4
- const isFancy =
5
- !process.env.NO_COLOR &&
6
- process.env.TERM !== "dumb" &&
7
- process.stdout.isTTY === true
8
-
9
- const S = isFancy
10
- ? { ok: "✓", fail: "✗", run: "›", retry: "↻", skip: "⊘", warn: "⚠", bullet: "·" }
11
- : { ok: "+", fail: "x", run: ">", retry: "~", skip: "-", warn: "!", bullet: "*" }
12
-
13
- const C = isFancy
14
- ? {
15
- reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
16
- cyan: "\x1b[36m", green: "\x1b[32m",
17
- yellow: "\x1b[33m", red: "\x1b[31m", purple: "\x1b[35m",
18
- }
19
- : {
20
- reset: "", bold: "", dim: "",
21
- cyan: "", green: "",
22
- yellow: "", red: "", purple: "",
23
- }
24
-
25
- export class ConsoleReporter {
26
- attach(eventBus: EventBus): () => void {
27
- return eventBus.subscribe((e) => this.handle(e))
28
- }
29
-
30
- private handle(e: MultiClawEvent): void {
31
- switch (e.type) {
32
- case "orchestration:start": {
33
- const line = (isFancy ? "═" : "=").repeat(50)
34
- const edge = isFancy ? ["╔", "║", "╚", "╗", "╝"] : ["+", "|", "+", "+", "+"]
35
- console.log(`\n${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`)
36
- console.log(`${edge[1]} ${S.run} ${e.payload.name.padEnd(46)}${edge[1]}`)
37
- console.log(`${edge[2]}${line}${edge[4]}${C.reset}`)
38
- console.log(`${C.cyan}${S.bullet} Agents: ${e.payload.totalAgents} Stages: ${e.payload.stages.length}${C.reset}\n`)
39
- break
40
- }
41
- case "stage:start": {
42
- const dash = (isFancy ? "─" : "-").repeat(16)
43
- console.log(`${C.cyan}${dash} Stage ${e.payload.stageIndex + 1} ${dash}${C.reset}`)
44
- break
45
- }
46
- case "agent:start": {
47
- console.log(`${C.purple}${S.run} [${e.payload.agentName}] starting...${C.reset}`)
48
- break
49
- }
50
- case "agent:output": {
51
- process.stdout.write(` ${e.payload.chunk}`)
52
- break
53
- }
54
- case "agent:complete": {
55
- const d = (e.payload.duration / 1000).toFixed(1)
56
- console.log(`${C.green}${S.ok} ${e.payload.agentName} done (${d}s)${C.reset}`)
57
- break
58
- }
59
- case "agent:failed": {
60
- console.log(`${C.red}${S.fail} ${e.payload.agentName} failed: ${e.payload.error}${C.reset}`)
61
- break
62
- }
63
- case "agent:skipped": {
64
- console.log(`${C.yellow}${S.skip} ${e.payload.agentName} skipped${C.reset}`)
65
- break
66
- }
67
- case "agent:retrying": {
68
- console.log(`${C.yellow}${S.retry} ${e.payload.agentName} retry ${e.payload.attempt}/${e.payload.maxAttempts}: ${e.payload.error}${C.reset}`)
69
- break
70
- }
71
- case "orchestration:complete": {
72
- const { success, totalDuration, agentResults } = e.payload
73
- const line = (isFancy ? "═" : "=").repeat(50)
74
- const edge = isFancy ? ["╔", "║", "╚", "╗", "╝"] : ["+", "|", "+", "+", "+"]
75
- const title = success ? `${S.ok} All done` : `${S.warn} Done (with failures)`
76
- console.log(`\n${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`)
77
- console.log(`${edge[1]} ${title.padEnd(48)}${edge[1]}`)
78
- console.log(`${edge[2]}${line}${edge[4]}${C.reset}`)
79
- const nameWidth = Math.max(8, ...agentResults.map((r) => r.agentName.length))
80
- const totalStr = `${(totalDuration / 1000).toFixed(1)}s`
81
- console.log(` ~ ${"Total".padEnd(nameWidth)} ${totalStr.padStart(7)}`)
82
- for (const r of agentResults) {
83
- const icon = r.status === "success" ? S.ok : S.fail
84
- const color = r.status === "success" ? C.green : C.red
85
- const dur = `${(r.duration / 1000).toFixed(1)}s`
86
- console.log(` ${color}${icon}${C.reset} ${r.agentName.padEnd(nameWidth)} ${dur.padStart(7)}`)
87
- }
88
- console.log()
89
- break
90
- }
91
- }
92
- }
93
- }
@@ -1,43 +0,0 @@
1
- import * as fs from "fs"
2
- import * as path from "path"
3
- import type { EventBus, MultiClawEvent } from "@multiclawcli/core"
4
-
5
- export class FileLogWriter {
6
- private streams = new Map<string, fs.WriteStream>()
7
-
8
- constructor(private logDir: string) {
9
- fs.mkdirSync(logDir, { recursive: true })
10
- }
11
-
12
- attach(eventBus: EventBus): () => void {
13
- return eventBus.subscribe((e) => this.handle(e))
14
- }
15
-
16
- private handle(e: MultiClawEvent): void {
17
- if (e.type === "agent:output") {
18
- this.getStream(e.payload.agentId).write(e.payload.chunk)
19
- }
20
- if (e.type === "orchestration:complete") {
21
- fs.writeFileSync(
22
- path.join(this.logDir, "report.json"),
23
- JSON.stringify(e.payload, null, 2)
24
- )
25
- this.closeAll()
26
- }
27
- }
28
-
29
- private getStream(agentId: string): fs.WriteStream {
30
- if (!this.streams.has(agentId)) {
31
- this.streams.set(
32
- agentId,
33
- fs.createWriteStream(path.join(this.logDir, `${agentId}.log`))
34
- )
35
- }
36
- return this.streams.get(agentId)!
37
- }
38
-
39
- private closeAll(): void {
40
- for (const s of this.streams.values()) s.end()
41
- this.streams.clear()
42
- }
43
- }
package/tsconfig.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.base.json",
3
- "compilerOptions": { "outDir": "dist", "rootDir": "src" },
4
- "references": [
5
- { "path": "../core" },
6
- { "path": "../dashboard/tsconfig.server.json" }
7
- ],
8
- "include": ["src"]
9
- }