multiclaw 0.3.1 → 0.4.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/dist/index.js CHANGED
@@ -1,19 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command } from "commander";
5
4
  import { readFileSync } from "fs";
6
5
  import { dirname as dirname2, join as join3 } from "path";
7
6
  import { fileURLToPath } from "url";
7
+ import { Command } from "commander";
8
8
 
9
- // src/commands/run.ts
10
- import * as path3 from "path";
11
- import { Orchestrator as OrchestratorImpl, runtimeRegistry } from "@multiclawcli/core";
9
+ // src/commands/doctor.ts
10
+ import {
11
+ detectProviders,
12
+ presentProviders,
13
+ resolveProvider,
14
+ specForProvider
15
+ } from "@multiclawcli/core";
12
16
 
13
17
  // src/loader/loadConfig.ts
14
- import * as path from "path";
15
18
  import * as fs from "fs";
19
+ import { createRequire } from "module";
20
+ import * as path from "path";
16
21
  var DEFAULT_CONFIG_NAMES = ["multiclaw.config.ts", "multiclaw.config.js"];
22
+ var CONFIG_HELPER_SPECIFIERS = ["multiclaw", "@multiclawcli/core"];
17
23
  function findConfig(configPath) {
18
24
  if (configPath) {
19
25
  const abs = path.resolve(process.cwd(), configPath);
@@ -24,19 +30,51 @@ function findConfig(configPath) {
24
30
  const abs = path.resolve(process.cwd(), name);
25
31
  if (fs.existsSync(abs)) return abs;
26
32
  }
27
- throw new Error(`No config file found. Create multiclaw.config.ts in the current directory or use --config to specify a path.`);
33
+ throw new Error(
34
+ `No config file found. Create multiclaw.config.ts in the current directory or use --config to specify a path.`
35
+ );
28
36
  }
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;
37
+ function resolveHelpersEntry() {
34
38
  try {
35
- mod = await import(absPath);
36
- } finally {
37
- unregister();
39
+ return createRequire(import.meta.url).resolve("@multiclawcli/core");
40
+ } catch {
41
+ return void 0;
42
+ }
43
+ }
44
+ async function importConfig(absPath) {
45
+ const { createJiti } = await import("jiti");
46
+ const helpers = resolveHelpersEntry();
47
+ const jiti = createJiti(import.meta.url, {
48
+ interopDefault: true,
49
+ ...helpers ? { alias: Object.fromEntries(CONFIG_HELPER_SPECIFIERS.map((name) => [name, helpers])) } : {}
50
+ });
51
+ try {
52
+ return await jiti.import(absPath);
53
+ } catch (err) {
54
+ const detail = err instanceof Error ? err.message : String(err);
55
+ throw new Error(
56
+ [
57
+ `Failed to load config: ${absPath}`,
58
+ ` ${detail.split("\n")[0]}`,
59
+ ` Check the file for syntax errors, and that every package it imports is installed.`
60
+ ].join("\n")
61
+ );
38
62
  }
39
- const config = mod.default ?? mod.config;
63
+ }
64
+ function unwrapConfig(mod) {
65
+ let value = mod;
66
+ for (let depth = 0; depth < 3; depth += 1) {
67
+ if (!value || typeof value !== "object" || Array.isArray(value)) break;
68
+ const record = value;
69
+ const next = "default" in record ? record.default : record.config;
70
+ if (!next || typeof next !== "object" || Array.isArray(next)) break;
71
+ value = next;
72
+ }
73
+ return value;
74
+ }
75
+ async function loadConfig(configPath, requirement) {
76
+ const absPath = findConfig(configPath);
77
+ const config = unwrapConfig(await importConfig(absPath));
40
78
  if (!config || !Array.isArray(config.agents)) {
41
79
  throw new Error(`Invalid config format \u2014 expected: export default defineConfig({...})`);
42
80
  }
@@ -48,6 +86,361 @@ async function loadConfig(configPath, requirement) {
48
86
  return config;
49
87
  }
50
88
 
89
+ // src/commands/doctor.ts
90
+ function buildDoctorReport(detected, config) {
91
+ const present = presentProviders(detected);
92
+ const providers = detected.map((d) => {
93
+ const spec = specForProvider(d.id);
94
+ return {
95
+ id: d.id,
96
+ label: d.label,
97
+ status: d.status,
98
+ command: d.command,
99
+ version: d.version,
100
+ modelCount: spec?.models.length ?? 0,
101
+ modelSelection: spec?.modelSelection ?? "none",
102
+ authHint: spec?.auth.hint ?? "",
103
+ installHint: d.status === "missing" ? spec?.install.hint : void 0
104
+ };
105
+ });
106
+ const installedCount = present.length;
107
+ let configReport;
108
+ if (config) {
109
+ const defaultsProvider = config.defaults?.provider;
110
+ const defaultsProviderOk = !defaultsProvider || present.includes(defaultsProvider);
111
+ const agents2 = [...config.agents];
112
+ if (config.leader) agents2.push(config.leader);
113
+ const agentsWithMissingProvider = agents2.map((a) => ({ id: a.id, intended: resolveProvider(a, config) })).filter((a) => !present.includes(a.intended)).map((a) => a.id);
114
+ configReport = {
115
+ name: config.name,
116
+ defaultsProvider,
117
+ defaultsProviderOk,
118
+ agentsWithMissingProvider
119
+ };
120
+ }
121
+ return {
122
+ providers,
123
+ installedCount,
124
+ config: configReport,
125
+ runnable: installedCount > 0
126
+ };
127
+ }
128
+ var GREEN = "\x1B[32m";
129
+ var RED = "\x1B[31m";
130
+ var YELLOW = "\x1B[33m";
131
+ var DIM = "\x1B[2m";
132
+ var RESET = "\x1B[0m";
133
+ var ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
134
+ function color(enabled, code, text) {
135
+ return enabled ? `${code}${text}${RESET}` : text;
136
+ }
137
+ function formatDoctorReport(report, useColor = true) {
138
+ const lines = [];
139
+ const c = (code, text) => color(useColor, code, text);
140
+ const cell = (text, width, code) => {
141
+ const rendered = code ? color(useColor, code, text) : text;
142
+ const visible = text.replace(ANSI_RE, "").length;
143
+ return rendered + " ".repeat(Math.max(0, width - visible));
144
+ };
145
+ lines.push("");
146
+ lines.push(c(DIM, "multiclaw doctor \u2014 environment check"));
147
+ lines.push("");
148
+ lines.push(`${cell("provider", 12)}${cell("status", 10)}${cell("version", 14)}models auth`);
149
+ for (const p of report.providers) {
150
+ const mark = p.status === "present" ? "\u2713 present" : "\u2717 missing";
151
+ const markCode = p.status === "present" ? GREEN : RED;
152
+ const version = p.version ?? (p.status === "present" ? "" : "\u2014");
153
+ const models = p.modelSelection === "none" ? "n/a" : String(p.modelCount);
154
+ const auth = p.authHint ? c(DIM, p.authHint) : "";
155
+ lines.push(
156
+ `${cell(p.label, 12)}${cell(mark, 10, markCode)}${cell(version, 14)}${cell(models, 7)}${auth}`
157
+ );
158
+ if (p.status === "missing" && p.installHint) {
159
+ lines.push(c(YELLOW, ` install: ${p.installHint}`));
160
+ }
161
+ }
162
+ if (report.config) {
163
+ lines.push("");
164
+ lines.push(c(DIM, `config: ${report.config.name ?? "(unnamed)"}`));
165
+ if (report.config.defaultsProvider) {
166
+ const ok = report.config.defaultsProviderOk;
167
+ lines.push(
168
+ ` default provider: ${report.config.defaultsProvider} ${ok ? c(GREEN, "\u2713 installed") : c(RED, "\u2717 not installed")}`
169
+ );
170
+ }
171
+ if (report.config.agentsWithMissingProvider.length > 0) {
172
+ lines.push(
173
+ c(
174
+ RED,
175
+ ` ${report.config.agentsWithMissingProvider.length} agent(s) need a provider that is not installed: ${report.config.agentsWithMissingProvider.join(", ")}`
176
+ )
177
+ );
178
+ } else {
179
+ lines.push(c(GREEN, " all agents resolve to an installed provider \u2713"));
180
+ }
181
+ }
182
+ lines.push("");
183
+ if (report.runnable) {
184
+ lines.push(
185
+ c(
186
+ GREEN,
187
+ `\u2713 ${report.installedCount} of ${report.providers.length} providers available \u2014 multiclaw can run.`
188
+ )
189
+ );
190
+ } else {
191
+ lines.push(c(RED, `\u2717 no providers installed \u2014 install one of the above to run multiclaw.`));
192
+ }
193
+ lines.push("");
194
+ return lines.join("\n");
195
+ }
196
+ async function doctorCommand(configPath) {
197
+ const detected = await detectProviders({ withVersion: true });
198
+ const config = await loadConfigSafe(configPath);
199
+ const report = buildDoctorReport(detected, config);
200
+ const useColor = !process.env.NO_COLOR && process.stdout.isTTY;
201
+ console.log(formatDoctorReport(report, useColor));
202
+ process.exitCode = report.runnable ? 0 : 1;
203
+ }
204
+ async function loadConfigSafe(configPath) {
205
+ try {
206
+ return await loadConfig(configPath);
207
+ } catch {
208
+ return void 0;
209
+ }
210
+ }
211
+
212
+ // src/commands/init.ts
213
+ import * as fs2 from "fs";
214
+ import * as path2 from "path";
215
+ import { checkbox, input, Separator, select } from "@inquirer/prompts";
216
+ import { detectProviders as detectProviders2, specForProvider as specForProvider2, tierModel } from "@multiclawcli/core";
217
+ var AGENT_BLOCKS = {
218
+ backend: ` agents.productManager(),
219
+ agents.architect({ dependsOn: ["product-manager"] }),
220
+ agents.archDesignReviewer({ dependsOn: ["architect"] }),
221
+ agents.backendDeveloper({ dependsOn: ["arch-design-reviewer"] }),
222
+ agents.tester({ dependsOn: ["backend-developer"] }),
223
+ agents.codeReviewer({ dependsOn: ["tester"] }),
224
+ agents.devops({ dependsOn: ["tester"] }),`,
225
+ frontend: ` agents.architect(),
226
+ agents.uiDesigner({ dependsOn: ["architect"] }),
227
+ agents.uiDesignReviewer({ dependsOn: ["ui-designer"] }),
228
+ agents.frontendDeveloper({ dependsOn: ["ui-design-reviewer"] }),
229
+ agents.tester({ dependsOn: ["frontend-developer"] }),
230
+ agents.codeReviewer({ dependsOn: ["tester"] }),`,
231
+ fullstack: ` agents.productManager(),
232
+ agents.architect({ dependsOn: ["product-manager"] }),
233
+ agents.archDesignReviewer({ dependsOn: ["architect"] }),
234
+ agents.backendDeveloper({ dependsOn: ["arch-design-reviewer"] }),
235
+ agents.uiDesigner({ dependsOn: ["arch-design-reviewer"] }),
236
+ agents.uiDesignReviewer({ dependsOn: ["ui-designer"] }),
237
+ agents.frontendDeveloper({ dependsOn: ["backend-developer", "ui-design-reviewer"] }),
238
+ agents.tester({ dependsOn: ["frontend-developer"] }),
239
+ agents.codeReviewer({ dependsOn: ["tester"] }),
240
+ agents.devops({ dependsOn: ["tester"] }),`
241
+ };
242
+ var PRESET_DESCRIPTIONS = {
243
+ backend: "Product Manager \u2192 Architect \u2192 Arch Design Reviewer \u2192 Backend Developer \u2192 Tester \u2192 Code Reviewer + DevOps (7 agents)",
244
+ frontend: "Architect \u2192 UI Designer \u2192 UI Design Reviewer \u2192 Frontend Developer \u2192 Tester \u2192 Code Reviewer (6 agents)",
245
+ fullstack: "Product Manager \u2192 Architect \u2192 Arch Design Reviewer \u2192 Backend Developer + UI Designer \u2192 UI Design Reviewer \u2192 Frontend Developer \u2192 Tester \u2192 Code Reviewer + DevOps (10 agents)"
246
+ };
247
+ function generateConfig(opts) {
248
+ const name = JSON.stringify(opts.projectName);
249
+ const workDir = JSON.stringify(opts.workDir);
250
+ const defaultsParts = [`provider: ${JSON.stringify(opts.provider)}`];
251
+ if (opts.model) defaultsParts.push(`model: ${JSON.stringify(opts.model)}`);
252
+ let agentBlock = AGENT_BLOCKS[opts.preset];
253
+ if (opts.overrides && opts.overrides.length > 0) {
254
+ for (const ov of opts.overrides) {
255
+ const id = ov.id;
256
+ const model = JSON.stringify(ov.model);
257
+ const re = new RegExp(`(agents\\.${id}\\([^)]*\\))`, "g");
258
+ agentBlock = agentBlock.replace(re, (match) => {
259
+ const inner = match.slice(id.length + 8, -1);
260
+ const sep = inner.trim() ? ", " : "";
261
+ return `agents.${id}({ ${inner}${sep}model: ${model} })`;
262
+ });
263
+ }
264
+ }
265
+ return `import { defineConfig, agents } from "multiclaw"
266
+
267
+ export default defineConfig({
268
+ name: ${name},
269
+ workDir: ${workDir},
270
+ context: {
271
+ projectName: ${name},
272
+ },
273
+ defaults: {
274
+ ${defaultsParts.join(",\n ")},
275
+ },
276
+ agents: [
277
+ ${agentBlock}
278
+ ],
279
+ })
280
+ `;
281
+ }
282
+ async function initCommand() {
283
+ const configPath = path2.resolve(process.cwd(), "multiclaw.config.ts");
284
+ if (fs2.existsSync(configPath)) {
285
+ console.error("\x1B[31mError: multiclaw.config.ts already exists\x1B[0m");
286
+ process.exit(1);
287
+ }
288
+ const defaultProjectName = path2.basename(process.cwd());
289
+ const projectName = await input({
290
+ message: "Project name:",
291
+ default: defaultProjectName
292
+ });
293
+ const preset = await select({
294
+ message: "Select a preset:",
295
+ choices: [
296
+ { value: "backend", name: `backend \u2014 ${PRESET_DESCRIPTIONS.backend}` },
297
+ { value: "frontend", name: `frontend \u2014 ${PRESET_DESCRIPTIONS.frontend}` },
298
+ { value: "fullstack", name: `fullstack \u2014 ${PRESET_DESCRIPTIONS.fullstack}` }
299
+ ]
300
+ });
301
+ const detected = await detectProviders2({ withVersion: true });
302
+ const provider = await select({
303
+ message: "Which agent?",
304
+ choices: detected.map((d) => ({
305
+ value: d.id,
306
+ name: d.status === "present" ? `${d.label} (${d.command}${d.version ? ` ${d.version}` : ""})` : `${d.label} (not installed)`,
307
+ disabled: false
308
+ }))
309
+ });
310
+ const spec = specForProvider2(provider);
311
+ if (!spec) {
312
+ throw new Error(`Unknown provider: ${provider}`);
313
+ }
314
+ const modelAnswer = await select({
315
+ message: "Team model preference:",
316
+ choices: [
317
+ { value: "", name: "Role-based tier defaults (recommended)" },
318
+ new Separator("---"),
319
+ ...[
320
+ { label: "Strong (architect, devops)", tier: "strong" },
321
+ { label: "Balanced (backend, frontend)", tier: "balanced" },
322
+ { label: "Fast (reviewer, tester)", tier: "fast" }
323
+ ].map((t) => {
324
+ const m = tierModel(spec, t.tier);
325
+ return m ? { value: m, name: `${t.label} \u2192 ${m}` } : void 0;
326
+ }).filter((x) => x !== void 0),
327
+ new Separator("---"),
328
+ ...spec.models.map((m) => ({ value: m.id, name: m.label }))
329
+ ]
330
+ });
331
+ const modelDefault = modelAnswer || void 0;
332
+ const overrideIds = await checkbox({
333
+ message: "Override model for specific agents? (space to select, enter to confirm)",
334
+ choices: [
335
+ { value: "leader", name: "\u2605 Leader" },
336
+ { value: "productManager", name: "Product Manager" },
337
+ { value: "architect", name: "Architect" },
338
+ { value: "archDesignReviewer", name: "Arch Design Reviewer" },
339
+ { value: "backendDeveloper", name: "Backend Developer" },
340
+ { value: "frontendDeveloper", name: "Frontend Developer" },
341
+ { value: "uiDesigner", name: "UI Designer" },
342
+ { value: "uiDesignReviewer", name: "UI Design Reviewer" },
343
+ { value: "tester", name: "Tester" },
344
+ { value: "codeReviewer", name: "Code Reviewer" },
345
+ { value: "devops", name: "DevOps" }
346
+ ]
347
+ });
348
+ const overrides = [];
349
+ for (const id of overrideIds) {
350
+ const ov = await select({
351
+ message: `Model for ${id}:`.replace(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase()),
352
+ choices: spec.models.map((m) => ({ value: m.id, name: m.label }))
353
+ });
354
+ overrides.push({ id, model: ov });
355
+ }
356
+ const workDir = await input({
357
+ message: "Work directory (where agents read and write files):",
358
+ default: "."
359
+ });
360
+ try {
361
+ fs2.writeFileSync(
362
+ configPath,
363
+ generateConfig({ preset, workDir, projectName, provider, model: modelDefault, overrides })
364
+ );
365
+ if (workDir !== ".") {
366
+ fs2.mkdirSync(path2.resolve(process.cwd(), workDir), { recursive: true });
367
+ }
368
+ } catch (err) {
369
+ console.error(`\x1B[31mError: Failed to create files: ${err.message}\x1B[0m`);
370
+ process.exit(1);
371
+ }
372
+ const detectedProvider = detected.find((d) => d.id === provider);
373
+ console.log("");
374
+ console.log(`\x1B[32m\u2713\x1B[0m Created multiclaw.config.ts`);
375
+ console.log(` preset: ${preset} (${PRESET_DESCRIPTIONS[preset]})`);
376
+ console.log(` agent: ${spec.label}`);
377
+ if (detectedProvider?.status === "present") {
378
+ console.log(
379
+ ` ${detectedProvider.command}${detectedProvider.version ? ` ${detectedProvider.version}` : ""}`
380
+ );
381
+ }
382
+ console.log(` model: ${modelDefault || "role-based tier defaults"}`);
383
+ console.log(` workDir: ${workDir}`);
384
+ if (overrides.length > 0) {
385
+ console.log(` overrides: ${overrides.map((o) => `${o.id}=${o.model}`).join(", ")}`);
386
+ }
387
+ console.log("");
388
+ if (detectedProvider?.status === "missing") {
389
+ console.log(`\x1B[33m\u26A0 ${spec.label} is not installed.\x1B[0m`);
390
+ console.log(` ${spec.install.hint}`);
391
+ console.log("");
392
+ }
393
+ if (detectedProvider?.note) {
394
+ console.log(`\x1B[2m Before your first run: ${detectedProvider.note}\x1B[0m`);
395
+ console.log("");
396
+ }
397
+ console.log(`> Run: multiclaw (interactive console)`);
398
+ console.log(` multiclaw run "..." (headless)`);
399
+ console.log("");
400
+ console.log(
401
+ `\x1B[2mTip: the CLI resolves the "multiclaw" import itself, so the config runs either way.\x1B[0m`
402
+ );
403
+ console.log(
404
+ `\x1B[2m \`npm i -D multiclaw\` adds editor autocomplete for defineConfig/agents.\x1B[0m`
405
+ );
406
+ }
407
+
408
+ // src/commands/run.ts
409
+ import * as path4 from "path";
410
+ import { Orchestrator as OrchestratorImpl } from "@multiclawcli/core";
411
+
412
+ // src/lib.ts
413
+ import {
414
+ detectProviders as detectProviders3,
415
+ presentProviders as presentProviders2,
416
+ resolveCommand,
417
+ runtimeRegistry
418
+ } from "@multiclawcli/core";
419
+ import { agents, defineConfig } from "@multiclawcli/core";
420
+ async function assertRuntimesAvailable(config) {
421
+ const checked = /* @__PURE__ */ new Set();
422
+ for (const agent of config.agents) {
423
+ const name = agent.runtime ?? "acp";
424
+ if (!runtimeRegistry.has(name)) {
425
+ const supported = runtimeRegistry.list().join(", ");
426
+ console.error(`\x1B[31mError: Unknown runtime "${name}". Supported: ${supported}\x1B[0m`);
427
+ process.exit(1);
428
+ }
429
+ const command = resolveCommand(agent, config);
430
+ const key = `${name}\0${command}`;
431
+ if (checked.has(key)) continue;
432
+ checked.add(key);
433
+ const info = await runtimeRegistry.get(name).describe({ command });
434
+ if (!info.available) {
435
+ const subject = `Command "${command}"`;
436
+ console.error(
437
+ `\x1B[31mError: ${subject} is not available. Make sure the CLI is installed.\x1B[0m`
438
+ );
439
+ process.exit(1);
440
+ }
441
+ }
442
+ }
443
+
51
444
  // src/reporters/ConsoleReporter.ts
52
445
  var isFancy = !process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY === true;
53
446
  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: "*" };
@@ -83,8 +476,16 @@ var ConsoleReporter = class {
83
476
  ${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
84
477
  console.log(`${edge[1]} ${S.run} ${e.payload.name.padEnd(46)}${edge[1]}`);
85
478
  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
- `);
479
+ console.log(
480
+ `${C.cyan}${S.bullet} Agents: ${e.payload.totalAgents} Stages: ${e.payload.stages.length}${C.reset}
481
+ `
482
+ );
483
+ break;
484
+ }
485
+ case "orchestration:stages": {
486
+ console.log(
487
+ `${C.dim}${S.bullet} Plan applied \u2014 ${e.payload.stages.length} stages${C.reset}`
488
+ );
88
489
  break;
89
490
  }
90
491
  case "stage:start": {
@@ -114,14 +515,25 @@ ${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
114
515
  break;
115
516
  }
116
517
  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}`);
518
+ console.log(
519
+ `${C.yellow}${S.retry} ${e.payload.agentName} retry ${e.payload.attempt}/${e.payload.maxAttempts}: ${e.payload.error}${C.reset}`
520
+ );
521
+ break;
522
+ }
523
+ case "orchestration:warning": {
524
+ console.log(`${C.yellow}${S.warn} ${e.payload.message}${C.reset}`);
118
525
  break;
119
526
  }
120
527
  case "orchestration:complete": {
121
- const { success, totalDuration, agentResults } = e.payload;
528
+ const { success, totalDuration, agentResults, reply } = e.payload;
529
+ if (reply) {
530
+ const mark = reply.mode === "ask" ? "?" : S.ok;
531
+ console.log(`
532
+ ${C.cyan}${mark} ${reply.message}${C.reset}`);
533
+ }
122
534
  const line = (isFancy ? "\u2550" : "=").repeat(50);
123
535
  const edge = isFancy ? ["\u2554", "\u2551", "\u255A", "\u2557", "\u255D"] : ["+", "|", "+", "+", "+"];
124
- const title = success ? `${S.ok} All done` : `${S.warn} Done (with failures)`;
536
+ const title = reply ? reply.mode === "ask" ? `${S.warn} Needs your input` : `${S.ok} Answered` : success ? `${S.ok} All done` : `${S.warn} Done (with failures)`;
125
537
  console.log(`
126
538
  ${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
127
539
  console.log(`${edge[1]} ${title.padEnd(48)}${edge[1]}`);
@@ -131,9 +543,11 @@ ${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
131
543
  console.log(` ~ ${"Total".padEnd(nameWidth)} ${totalStr.padStart(7)}`);
132
544
  for (const r of agentResults) {
133
545
  const icon = r.status === "success" ? S.ok : S.fail;
134
- const color = r.status === "success" ? C.green : C.red;
546
+ const color2 = r.status === "success" ? C.green : C.red;
135
547
  const dur = `${(r.duration / 1e3).toFixed(1)}s`;
136
- console.log(` ${color}${icon}${C.reset} ${r.agentName.padEnd(nameWidth)} ${dur.padStart(7)}`);
548
+ console.log(
549
+ ` ${color2}${icon}${C.reset} ${r.agentName.padEnd(nameWidth)} ${dur.padStart(7)}`
550
+ );
137
551
  }
138
552
  console.log();
139
553
  break;
@@ -143,250 +557,136 @@ ${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
143
557
  };
144
558
 
145
559
  // src/reporters/FileLogWriter.ts
146
- import * as fs2 from "fs";
147
- import * as path2 from "path";
560
+ import * as fs3 from "fs";
561
+ import * as path3 from "path";
148
562
  var FileLogWriter = class {
149
- constructor(logDir) {
563
+ constructor(logDir, onError = (message) => process.stderr.write(`
564
+ [multiclaw] ${message}
565
+ `)) {
150
566
  this.logDir = logDir;
567
+ this.onError = onError;
151
568
  this.streams = /* @__PURE__ */ new Map();
152
- fs2.mkdirSync(logDir, { recursive: true });
569
+ /** Agent ids whose log could not be opened — never retried, so a broken
570
+ * directory does not produce one failing stream per output chunk. */
571
+ this.failed = /* @__PURE__ */ new Set();
572
+ fs3.mkdirSync(logDir, { recursive: true });
153
573
  }
154
574
  attach(eventBus) {
155
575
  return eventBus.subscribe((e) => this.handle(e));
156
576
  }
577
+ /**
578
+ * Flush every open stream.
579
+ *
580
+ * `stream.end()` only *starts* the flush, so a caller that exits the moment
581
+ * `orchestration:complete` arrives can lose the tail of the last agent's log.
582
+ * Awaiting this is what makes that tail durable.
583
+ */
584
+ async drain() {
585
+ if (!this.closing) this.closing = this.flush();
586
+ await this.closing;
587
+ }
157
588
  handle(e) {
158
589
  if (e.type === "agent:output") {
159
- this.getStream(e.payload.agentId).write(e.payload.chunk);
590
+ this.getStream(e.payload.agentId)?.write(e.payload.chunk);
160
591
  }
161
592
  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();
593
+ try {
594
+ fs3.writeFileSync(path3.join(this.logDir, "report.json"), JSON.stringify(e.payload, null, 2));
595
+ } catch (err) {
596
+ this.onError(`Could not write report.json: ${errorMessage(err)}`);
597
+ }
598
+ this.closing ??= this.flush();
167
599
  }
168
600
  }
169
601
  getStream(agentId) {
170
- if (!this.streams.has(agentId)) {
171
- this.streams.set(
172
- agentId,
173
- fs2.createWriteStream(path2.join(this.logDir, `${agentId}.log`))
174
- );
602
+ if (this.failed.has(agentId)) return void 0;
603
+ let stream = this.streams.get(agentId);
604
+ if (!stream) {
605
+ stream = fs3.createWriteStream(path3.join(this.logDir, `${agentId}.log`));
606
+ stream.on("error", (err) => {
607
+ this.failed.add(agentId);
608
+ this.streams.delete(agentId);
609
+ this.onError(`Could not write ${agentId}.log: ${errorMessage(err)}`);
610
+ });
611
+ this.streams.set(agentId, stream);
175
612
  }
176
- return this.streams.get(agentId);
613
+ return stream;
177
614
  }
178
- closeAll() {
179
- for (const s of this.streams.values()) s.end();
615
+ flush() {
616
+ const streams = [...this.streams.values()];
180
617
  this.streams.clear();
618
+ return Promise.all(
619
+ streams.map(
620
+ (s) => new Promise((resolve3) => {
621
+ if (s.destroyed || s.writableEnded) {
622
+ resolve3();
623
+ return;
624
+ }
625
+ s.end(() => resolve3());
626
+ })
627
+ )
628
+ ).then(() => void 0);
181
629
  }
182
630
  };
631
+ function errorMessage(err) {
632
+ return err instanceof Error ? err.message : String(err);
633
+ }
183
634
 
184
635
  // src/commands/run.ts
636
+ var EXIT_CANCELLED = 130;
185
637
  async function runCommand(requirement, options) {
186
638
  const config = await loadConfig(options.config, requirement);
187
639
  if (options.noLeader) {
188
640
  config.useLeader = false;
189
641
  }
190
642
  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
- }
643
+ config.workDir = path4.join(config.workDir, ".multiclaw", "runs", `run-${ts}`);
644
+ await assertRuntimesAvailable(config);
206
645
  const orchestrator = new OrchestratorImpl(config);
207
646
  new ConsoleReporter().attach(orchestrator.eventBus);
208
- const logDir = config.logDir ?? path3.join(config.workDir, "logs");
209
- new FileLogWriter(logDir).attach(orchestrator.eventBus);
647
+ const logDir = config.logDir ?? path4.join(config.workDir, "logs");
648
+ const logs = new FileLogWriter(logDir);
649
+ logs.attach(orchestrator.eventBus);
210
650
  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
- });
651
+ const controller = new AbortController();
652
+ const onSigint = () => {
653
+ if (controller.signal.aborted) process.exit(EXIT_CANCELLED);
654
+ controller.abort();
655
+ };
656
+ process.on("SIGINT", onSigint);
333
657
  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);
658
+ const result = await orchestrator.run({ signal: controller.signal });
659
+ await logs.drain();
660
+ process.exitCode = result.cancelled ? EXIT_CANCELLED : result.success ? 0 : 1;
661
+ } finally {
662
+ process.off("SIGINT", onSigint);
341
663
  }
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
664
  }
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;
665
+
666
+ // src/commands/tui.ts
667
+ async function tuiCommand(options) {
668
+ const config = await loadConfig(options.config);
669
+ await assertRuntimesAvailable(config);
670
+ const { startTui } = await import("@multiclawcli/dashboard/tui");
671
+ await startTui({
672
+ config,
673
+ onTurnStart: (eventBus, logDir) => {
674
+ new FileLogWriter(logDir).attach(eventBus);
358
675
  }
359
676
  });
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
677
  }
378
678
 
379
679
  // src/commands/upgrade.ts
380
- import { execSync as execSync2 } from "child_process";
381
- import { createRequire } from "module";
382
- var require2 = createRequire(import.meta.url);
680
+ import { execSync } from "child_process";
681
+ import { createRequire as createRequire2 } from "module";
682
+ var require2 = createRequire2(import.meta.url);
383
683
  async function upgradeCommand() {
384
- const { version: currentVersion } = require2("../../package.json");
684
+ const { version: currentVersion } = require2("../package.json");
385
685
  console.log(`Current version: v${currentVersion}`);
386
686
  console.log("Checking for updates...");
387
687
  let latestVersion;
388
688
  try {
389
- latestVersion = execSync2("npm show multiclaw version", { encoding: "utf8" }).trim();
689
+ latestVersion = execSync("npm show multiclaw version", { encoding: "utf8" }).trim();
390
690
  } catch {
391
691
  console.error("\x1B[31mError: Failed to fetch latest version from npm registry\x1B[0m");
392
692
  process.exit(1);
@@ -398,10 +698,12 @@ async function upgradeCommand() {
398
698
  }
399
699
  console.log(`Upgrading multiclaw v${currentVersion} \u2192 v${latestVersion}...`);
400
700
  try {
401
- execSync2("npm install -g multiclaw@latest", { stdio: "inherit" });
701
+ execSync("npm install -g multiclaw@latest", { stdio: "inherit" });
402
702
  console.log(`\x1B[32m\u2713\x1B[0m Successfully upgraded to v${latestVersion}`);
403
703
  } catch {
404
- console.error("\x1B[31mError: Upgrade failed. Try running manually: npm install -g multiclaw@latest\x1B[0m");
704
+ console.error(
705
+ "\x1B[31mError: Upgrade failed. Try running manually: npm install -g multiclaw@latest\x1B[0m"
706
+ );
405
707
  process.exit(1);
406
708
  }
407
709
  }
@@ -411,12 +713,13 @@ var __filename2 = fileURLToPath(import.meta.url);
411
713
  var __dirname2 = dirname2(__filename2);
412
714
  var pkg = JSON.parse(readFileSync(join3(__dirname2, "../package.json"), "utf-8"));
413
715
  var program = new Command();
414
- program.name("multiclaw").description("Multi-agent orchestration CLI").version(pkg.version, "-v, --version", "Display version number");
716
+ program.name("multiclaw").description("Multi-agent orchestration CLI").version(pkg.version, "-v, --version", "Display version number").action((options) => tuiCommand(options));
415
717
  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));
718
+ program.command("run <requirement>").description("Run an orchestration headlessly").option("--config <path>", "Config file path (default: auto-discover ./multiclaw.config.ts)").option("--no-leader", "Batch mode only: skip routing and run the full team").action((requirement, opts) => runCommand(requirement, opts));
418
719
  program.command("upgrade").description("Upgrade multiclaw to the latest version").action(() => upgradeCommand());
720
+ program.command("doctor").description("Check which agent providers are installed and ready").option("--config <path>", "Config file path (default: auto-discover ./multiclaw.config.ts)").action((opts) => doctorCommand(opts.config));
419
721
  program.parseAsync(process.argv).catch((err) => {
420
- console.error(`\x1B[31mError: ${err.message ?? err}\x1B[0m`);
722
+ const message = err instanceof Error ? err.message : String(err);
723
+ console.error(`\x1B[31mError: ${message}\x1B[0m`);
421
724
  process.exit(1);
422
725
  });