multiclaw 0.3.2 → 0.5.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/README.md +192 -50
- package/dist/commands/doctor.d.ts +32 -0
- package/dist/commands/doctor.test.d.ts +1 -0
- package/dist/commands/init.d.ts +32 -0
- package/dist/commands/init.test.d.ts +1 -0
- package/dist/commands/resume.d.ts +32 -0
- package/dist/commands/resume.test.d.ts +1 -0
- package/dist/commands/run.d.ts +12 -0
- package/dist/commands/run.test.d.ts +1 -0
- package/dist/commands/tui.d.ts +4 -0
- package/dist/commands/upgrade.d.ts +1 -0
- package/dist/commands/upgrade.test.d.ts +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +775 -228
- package/dist/lib.d.ts +20 -1
- package/dist/lib.js +40 -2
- package/dist/lib.test.d.ts +1 -0
- package/dist/loader/loadConfig.d.ts +2 -0
- package/dist/loader/loadConfig.test.d.ts +1 -0
- package/dist/reporters/ConsoleReporter.d.ts +5 -0
- package/dist/reporters/ConsoleReporter.test.d.ts +1 -0
- package/dist/reporters/FileLogWriter.d.ts +29 -0
- package/dist/reporters/FileLogWriter.test.d.ts +1 -0
- package/package.json +15 -11
package/dist/index.js
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { dirname as dirname2, join as join3 } from "path";
|
|
4
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
7
6
|
import { fileURLToPath } from "url";
|
|
7
|
+
import { Command } from "commander";
|
|
8
8
|
|
|
9
|
-
// src/commands/
|
|
10
|
-
import
|
|
11
|
-
|
|
9
|
+
// src/commands/doctor.ts
|
|
10
|
+
import {
|
|
11
|
+
detectProviders,
|
|
12
|
+
presentProviders,
|
|
13
|
+
resolveCommand,
|
|
14
|
+
resolveProvider,
|
|
15
|
+
specForCommand,
|
|
16
|
+
specForProvider
|
|
17
|
+
} from "@multiclawcli/core";
|
|
12
18
|
|
|
13
19
|
// src/loader/loadConfig.ts
|
|
14
|
-
import * as path from "path";
|
|
15
20
|
import * as fs from "fs";
|
|
21
|
+
import { createRequire } from "module";
|
|
22
|
+
import * as path from "path";
|
|
16
23
|
var DEFAULT_CONFIG_NAMES = ["multiclaw.config.ts", "multiclaw.config.js"];
|
|
24
|
+
var CONFIG_HELPER_SPECIFIERS = ["multiclaw", "@multiclawcli/core"];
|
|
17
25
|
function findConfig(configPath) {
|
|
18
26
|
if (configPath) {
|
|
19
27
|
const abs = path.resolve(process.cwd(), configPath);
|
|
@@ -24,19 +32,51 @@ function findConfig(configPath) {
|
|
|
24
32
|
const abs = path.resolve(process.cwd(), name);
|
|
25
33
|
if (fs.existsSync(abs)) return abs;
|
|
26
34
|
}
|
|
27
|
-
throw new Error(
|
|
35
|
+
throw new Error(
|
|
36
|
+
`No config file found. Create multiclaw.config.ts in the current directory or use --config to specify a path.`
|
|
37
|
+
);
|
|
28
38
|
}
|
|
29
|
-
|
|
30
|
-
const absPath = findConfig(configPath);
|
|
31
|
-
const { register } = await import("tsx/esm/api");
|
|
32
|
-
const unregister = register();
|
|
33
|
-
let mod;
|
|
39
|
+
function resolveHelpersEntry() {
|
|
34
40
|
try {
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
41
|
+
return createRequire(import.meta.url).resolve("@multiclawcli/core");
|
|
42
|
+
} catch {
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function importConfig(absPath) {
|
|
47
|
+
const { createJiti } = await import("jiti");
|
|
48
|
+
const helpers = resolveHelpersEntry();
|
|
49
|
+
const jiti = createJiti(import.meta.url, {
|
|
50
|
+
interopDefault: true,
|
|
51
|
+
...helpers ? { alias: Object.fromEntries(CONFIG_HELPER_SPECIFIERS.map((name) => [name, helpers])) } : {}
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
return await jiti.import(absPath);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
57
|
+
throw new Error(
|
|
58
|
+
[
|
|
59
|
+
`Failed to load config: ${absPath}`,
|
|
60
|
+
` ${detail.split("\n")[0]}`,
|
|
61
|
+
` Check the file for syntax errors, and that every package it imports is installed.`
|
|
62
|
+
].join("\n")
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function unwrapConfig(mod) {
|
|
67
|
+
let value = mod;
|
|
68
|
+
for (let depth = 0; depth < 3; depth += 1) {
|
|
69
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) break;
|
|
70
|
+
const record = value;
|
|
71
|
+
const next = "default" in record ? record.default : record.config;
|
|
72
|
+
if (!next || typeof next !== "object" || Array.isArray(next)) break;
|
|
73
|
+
value = next;
|
|
38
74
|
}
|
|
39
|
-
|
|
75
|
+
return value;
|
|
76
|
+
}
|
|
77
|
+
async function loadConfig(configPath, requirement) {
|
|
78
|
+
const absPath = findConfig(configPath);
|
|
79
|
+
const config = unwrapConfig(await importConfig(absPath));
|
|
40
80
|
if (!config || !Array.isArray(config.agents)) {
|
|
41
81
|
throw new Error(`Invalid config format \u2014 expected: export default defineConfig({...})`);
|
|
42
82
|
}
|
|
@@ -48,6 +88,431 @@ async function loadConfig(configPath, requirement) {
|
|
|
48
88
|
return config;
|
|
49
89
|
}
|
|
50
90
|
|
|
91
|
+
// src/commands/doctor.ts
|
|
92
|
+
function buildDoctorReport(detected, config) {
|
|
93
|
+
const present = presentProviders(detected);
|
|
94
|
+
const providers = detected.map((d) => {
|
|
95
|
+
const spec = specForProvider(d.id);
|
|
96
|
+
return {
|
|
97
|
+
id: d.id,
|
|
98
|
+
label: d.label,
|
|
99
|
+
status: d.status,
|
|
100
|
+
command: d.command,
|
|
101
|
+
source: d.source,
|
|
102
|
+
resolvedPath: d.resolvedPath,
|
|
103
|
+
version: d.version,
|
|
104
|
+
modelCount: spec?.models.length ?? 0,
|
|
105
|
+
modelSelection: spec?.modelSelection ?? "none",
|
|
106
|
+
authHint: spec?.auth.hint ?? "",
|
|
107
|
+
installHint: d.status === "missing" ? spec?.install.hint : void 0
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
const installedCount = present.length;
|
|
111
|
+
let configReport;
|
|
112
|
+
if (config) {
|
|
113
|
+
const defaultsProvider = config.defaults?.provider;
|
|
114
|
+
const defaultsProviderOk = !defaultsProvider || present.includes(defaultsProvider);
|
|
115
|
+
const agents2 = [...config.agents];
|
|
116
|
+
if (config.leader) agents2.push(config.leader);
|
|
117
|
+
const agentsWithMissingProvider = agents2.map((a) => ({ id: a.id, intended: resolveProvider(a, config) })).filter((a) => !present.includes(a.intended)).map((a) => a.id);
|
|
118
|
+
const agentsWithUnsupportedTools = agents2.map((a) => {
|
|
119
|
+
const command = resolveCommand(a, config);
|
|
120
|
+
const spec = specForCommand(command) ?? specForProvider(resolveProvider(a, config));
|
|
121
|
+
return { id: a.id, spec, tools: a.tools };
|
|
122
|
+
}).filter((a) => a.spec && !a.spec.tools && a.tools !== void 0).map((a) => a.id);
|
|
123
|
+
configReport = {
|
|
124
|
+
name: config.name,
|
|
125
|
+
defaultsProvider,
|
|
126
|
+
defaultsProviderOk,
|
|
127
|
+
agentsWithMissingProvider,
|
|
128
|
+
agentsWithUnsupportedTools
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
providers,
|
|
133
|
+
installedCount,
|
|
134
|
+
config: configReport,
|
|
135
|
+
runnable: installedCount > 0
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
var GREEN = "\x1B[32m";
|
|
139
|
+
var RED = "\x1B[31m";
|
|
140
|
+
var YELLOW = "\x1B[33m";
|
|
141
|
+
var DIM = "\x1B[2m";
|
|
142
|
+
var RESET = "\x1B[0m";
|
|
143
|
+
var ANSI_RE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g");
|
|
144
|
+
function color(enabled, code, text) {
|
|
145
|
+
return enabled ? `${code}${text}${RESET}` : text;
|
|
146
|
+
}
|
|
147
|
+
function formatDoctorReport(report, useColor = true) {
|
|
148
|
+
const lines = [];
|
|
149
|
+
const c = (code, text) => color(useColor, code, text);
|
|
150
|
+
const cell = (text, width, code) => {
|
|
151
|
+
const rendered = code ? color(useColor, code, text) : text;
|
|
152
|
+
const visible = text.replace(ANSI_RE, "").length;
|
|
153
|
+
return rendered + " ".repeat(Math.max(0, width - visible));
|
|
154
|
+
};
|
|
155
|
+
lines.push("");
|
|
156
|
+
lines.push(c(DIM, "multiclaw doctor \u2014 environment check"));
|
|
157
|
+
lines.push("");
|
|
158
|
+
lines.push(
|
|
159
|
+
`${cell("provider", 12)}${cell("status", 10)}${cell("version", 14)}${cell("source", 9)}models auth`
|
|
160
|
+
);
|
|
161
|
+
for (const p of report.providers) {
|
|
162
|
+
const mark = p.status === "present" ? "\u2713 present" : "\u2717 missing";
|
|
163
|
+
const markCode = p.status === "present" ? GREEN : RED;
|
|
164
|
+
const version = p.version ?? (p.status === "present" ? "" : "\u2014");
|
|
165
|
+
const source = p.source ?? "\u2014";
|
|
166
|
+
const sourceCode = p.source === "bundled" ? YELLOW : void 0;
|
|
167
|
+
const models = p.modelSelection === "none" ? "n/a" : String(p.modelCount);
|
|
168
|
+
const auth = p.authHint ? c(DIM, p.authHint) : "";
|
|
169
|
+
lines.push(
|
|
170
|
+
`${cell(p.label, 12)}${cell(mark, 10, markCode)}${cell(version, 14)}${cell(source, 9, sourceCode)}${cell(models, 7)}${auth}`
|
|
171
|
+
);
|
|
172
|
+
if (p.status === "present" && p.source === "bundled" && p.resolvedPath) {
|
|
173
|
+
lines.push(c(DIM, ` bundled bridge: ${p.resolvedPath}`));
|
|
174
|
+
}
|
|
175
|
+
if (p.status === "missing" && p.installHint) {
|
|
176
|
+
lines.push(c(YELLOW, ` install: ${p.installHint}`));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (report.config) {
|
|
180
|
+
lines.push("");
|
|
181
|
+
lines.push(c(DIM, `config: ${report.config.name ?? "(unnamed)"}`));
|
|
182
|
+
if (report.config.defaultsProvider) {
|
|
183
|
+
const ok = report.config.defaultsProviderOk;
|
|
184
|
+
lines.push(
|
|
185
|
+
` default provider: ${report.config.defaultsProvider} ${ok ? c(GREEN, "\u2713 installed") : c(RED, "\u2717 not installed")}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (report.config.agentsWithMissingProvider.length > 0) {
|
|
189
|
+
lines.push(
|
|
190
|
+
c(
|
|
191
|
+
RED,
|
|
192
|
+
` ${report.config.agentsWithMissingProvider.length} agent(s) need a provider that is not installed: ${report.config.agentsWithMissingProvider.join(", ")}`
|
|
193
|
+
)
|
|
194
|
+
);
|
|
195
|
+
} else {
|
|
196
|
+
lines.push(c(GREEN, " all agents resolve to an installed provider \u2713"));
|
|
197
|
+
}
|
|
198
|
+
if (report.config.agentsWithUnsupportedTools.length > 0) {
|
|
199
|
+
lines.push(
|
|
200
|
+
c(
|
|
201
|
+
YELLOW,
|
|
202
|
+
` ${report.config.agentsWithUnsupportedTools.length} agent(s) use a provider that cannot enforce tool allowlists: ${report.config.agentsWithUnsupportedTools.join(", ")}`
|
|
203
|
+
)
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
lines.push("");
|
|
208
|
+
if (report.runnable) {
|
|
209
|
+
lines.push(
|
|
210
|
+
c(
|
|
211
|
+
GREEN,
|
|
212
|
+
`\u2713 ${report.installedCount} of ${report.providers.length} providers available \u2014 multiclaw can run.`
|
|
213
|
+
)
|
|
214
|
+
);
|
|
215
|
+
} else {
|
|
216
|
+
lines.push(c(RED, `\u2717 no providers installed \u2014 install one of the above to run multiclaw.`));
|
|
217
|
+
}
|
|
218
|
+
lines.push("");
|
|
219
|
+
return lines.join("\n");
|
|
220
|
+
}
|
|
221
|
+
async function doctorCommand(configPath) {
|
|
222
|
+
const detected = await detectProviders({ withVersion: true });
|
|
223
|
+
const config = await loadConfigSafe(configPath);
|
|
224
|
+
const report = buildDoctorReport(detected, config);
|
|
225
|
+
const useColor = !process.env.NO_COLOR && process.stdout.isTTY;
|
|
226
|
+
console.log(formatDoctorReport(report, useColor));
|
|
227
|
+
process.exitCode = report.runnable ? 0 : 1;
|
|
228
|
+
}
|
|
229
|
+
async function loadConfigSafe(configPath) {
|
|
230
|
+
try {
|
|
231
|
+
return await loadConfig(configPath);
|
|
232
|
+
} catch {
|
|
233
|
+
return void 0;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/commands/init.ts
|
|
238
|
+
import * as fs2 from "fs";
|
|
239
|
+
import * as path2 from "path";
|
|
240
|
+
import { checkbox, input, Separator, select } from "@inquirer/prompts";
|
|
241
|
+
import { detectProviders as detectProviders2, specForProvider as specForProvider2, tierModel } from "@multiclawcli/core";
|
|
242
|
+
var AGENT_BLOCKS = {
|
|
243
|
+
backend: ` agents.productManager(),
|
|
244
|
+
agents.architect({ dependsOn: ["product-manager"] }),
|
|
245
|
+
agents.archDesignReviewer({ dependsOn: ["architect"] }),
|
|
246
|
+
agents.backendDeveloper({ dependsOn: ["arch-design-reviewer"] }),
|
|
247
|
+
agents.tester({ dependsOn: ["backend-developer"] }),
|
|
248
|
+
agents.codeReviewer({ dependsOn: ["tester"] }),
|
|
249
|
+
agents.devops({ dependsOn: ["tester"] }),`,
|
|
250
|
+
frontend: ` agents.architect(),
|
|
251
|
+
agents.uiDesigner({ dependsOn: ["architect"] }),
|
|
252
|
+
agents.uiDesignReviewer({ dependsOn: ["ui-designer"] }),
|
|
253
|
+
agents.frontendDeveloper({ dependsOn: ["ui-design-reviewer"] }),
|
|
254
|
+
agents.tester({ dependsOn: ["frontend-developer"] }),
|
|
255
|
+
agents.codeReviewer({ dependsOn: ["tester"] }),`,
|
|
256
|
+
fullstack: ` agents.productManager(),
|
|
257
|
+
agents.architect({ dependsOn: ["product-manager"] }),
|
|
258
|
+
agents.archDesignReviewer({ dependsOn: ["architect"] }),
|
|
259
|
+
agents.backendDeveloper({ dependsOn: ["arch-design-reviewer"] }),
|
|
260
|
+
agents.uiDesigner({ dependsOn: ["arch-design-reviewer"] }),
|
|
261
|
+
agents.uiDesignReviewer({ dependsOn: ["ui-designer"] }),
|
|
262
|
+
agents.frontendDeveloper({ dependsOn: ["backend-developer", "ui-design-reviewer"] }),
|
|
263
|
+
agents.tester({ dependsOn: ["frontend-developer"] }),
|
|
264
|
+
agents.codeReviewer({ dependsOn: ["tester"] }),
|
|
265
|
+
agents.devops({ dependsOn: ["tester"] }),`
|
|
266
|
+
};
|
|
267
|
+
var PRESET_DESCRIPTIONS = {
|
|
268
|
+
backend: "Product Manager \u2192 Architect \u2192 Arch Design Reviewer \u2192 Backend Developer \u2192 Tester \u2192 Code Reviewer + DevOps (7 agents)",
|
|
269
|
+
frontend: "Architect \u2192 UI Designer \u2192 UI Design Reviewer \u2192 Frontend Developer \u2192 Tester \u2192 Code Reviewer (6 agents)",
|
|
270
|
+
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)"
|
|
271
|
+
};
|
|
272
|
+
var PRESET_AGENT_IDS = {
|
|
273
|
+
backend: [
|
|
274
|
+
"productManager",
|
|
275
|
+
"architect",
|
|
276
|
+
"archDesignReviewer",
|
|
277
|
+
"backendDeveloper",
|
|
278
|
+
"tester",
|
|
279
|
+
"codeReviewer",
|
|
280
|
+
"devops"
|
|
281
|
+
],
|
|
282
|
+
frontend: [
|
|
283
|
+
"architect",
|
|
284
|
+
"uiDesigner",
|
|
285
|
+
"uiDesignReviewer",
|
|
286
|
+
"frontendDeveloper",
|
|
287
|
+
"tester",
|
|
288
|
+
"codeReviewer"
|
|
289
|
+
],
|
|
290
|
+
fullstack: [
|
|
291
|
+
"productManager",
|
|
292
|
+
"architect",
|
|
293
|
+
"archDesignReviewer",
|
|
294
|
+
"backendDeveloper",
|
|
295
|
+
"uiDesigner",
|
|
296
|
+
"uiDesignReviewer",
|
|
297
|
+
"frontendDeveloper",
|
|
298
|
+
"tester",
|
|
299
|
+
"codeReviewer",
|
|
300
|
+
"devops"
|
|
301
|
+
]
|
|
302
|
+
};
|
|
303
|
+
var OVERRIDE_LABELS = {
|
|
304
|
+
leader: "\u2605 Leader",
|
|
305
|
+
productManager: "Product Manager",
|
|
306
|
+
architect: "Architect",
|
|
307
|
+
archDesignReviewer: "Arch Design Reviewer",
|
|
308
|
+
backendDeveloper: "Backend Developer",
|
|
309
|
+
frontendDeveloper: "Frontend Developer",
|
|
310
|
+
uiDesigner: "UI Designer",
|
|
311
|
+
uiDesignReviewer: "UI Design Reviewer",
|
|
312
|
+
tester: "Tester",
|
|
313
|
+
codeReviewer: "Code Reviewer",
|
|
314
|
+
devops: "DevOps"
|
|
315
|
+
};
|
|
316
|
+
function overrideChoices(preset) {
|
|
317
|
+
const ids = PRESET_AGENT_IDS[preset] ?? [];
|
|
318
|
+
return [
|
|
319
|
+
{ value: "leader", name: OVERRIDE_LABELS.leader },
|
|
320
|
+
...ids.map((id) => ({ value: id, name: OVERRIDE_LABELS[id] ?? id }))
|
|
321
|
+
];
|
|
322
|
+
}
|
|
323
|
+
function generateConfig(opts) {
|
|
324
|
+
const name = JSON.stringify(opts.projectName);
|
|
325
|
+
const workDir = JSON.stringify(opts.workDir);
|
|
326
|
+
const defaultsParts = [`provider: ${JSON.stringify(opts.provider)}`];
|
|
327
|
+
if (opts.model) defaultsParts.push(`model: ${JSON.stringify(opts.model)}`);
|
|
328
|
+
const leaderOverride = opts.overrides?.find((o) => o.id === "leader");
|
|
329
|
+
const leaderLine = leaderOverride ? ` leader: agents.leader({ model: ${JSON.stringify(leaderOverride.model)} }),` : ` leader: agents.leader(),`;
|
|
330
|
+
let agentBlock = AGENT_BLOCKS[opts.preset];
|
|
331
|
+
if (opts.overrides && opts.overrides.length > 0) {
|
|
332
|
+
for (const ov of opts.overrides) {
|
|
333
|
+
const id = ov.id;
|
|
334
|
+
if (id === "leader") continue;
|
|
335
|
+
const model = JSON.stringify(ov.model);
|
|
336
|
+
const re = new RegExp(`(agents\\.${id}\\([^)]*\\))`, "g");
|
|
337
|
+
agentBlock = agentBlock.replace(re, (match) => {
|
|
338
|
+
const body = match.slice(id.length + 8, -1).trim().replace(/^\{/, "").replace(/\}$/, "").trim();
|
|
339
|
+
const sep = body ? ", " : "";
|
|
340
|
+
return `agents.${id}({ ${body}${sep}model: ${model} })`;
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return `import { defineConfig, agents } from "multiclaw"
|
|
345
|
+
|
|
346
|
+
export default defineConfig({
|
|
347
|
+
name: ${name},
|
|
348
|
+
workDir: ${workDir},
|
|
349
|
+
${leaderLine}
|
|
350
|
+
context: {
|
|
351
|
+
projectName: ${name},
|
|
352
|
+
},
|
|
353
|
+
defaults: {
|
|
354
|
+
${defaultsParts.join(",\n ")},
|
|
355
|
+
},
|
|
356
|
+
agents: [
|
|
357
|
+
${agentBlock}
|
|
358
|
+
],
|
|
359
|
+
})
|
|
360
|
+
`;
|
|
361
|
+
}
|
|
362
|
+
async function initCommand() {
|
|
363
|
+
const configPath = path2.resolve(process.cwd(), "multiclaw.config.ts");
|
|
364
|
+
if (fs2.existsSync(configPath)) {
|
|
365
|
+
console.error("\x1B[31mError: multiclaw.config.ts already exists\x1B[0m");
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
const defaultProjectName = path2.basename(process.cwd());
|
|
369
|
+
const projectName = await input({
|
|
370
|
+
message: "Project name:",
|
|
371
|
+
default: defaultProjectName
|
|
372
|
+
});
|
|
373
|
+
const preset = await select({
|
|
374
|
+
message: "Select a preset:",
|
|
375
|
+
choices: [
|
|
376
|
+
{ value: "backend", name: `backend \u2014 ${PRESET_DESCRIPTIONS.backend}` },
|
|
377
|
+
{ value: "frontend", name: `frontend \u2014 ${PRESET_DESCRIPTIONS.frontend}` },
|
|
378
|
+
{ value: "fullstack", name: `fullstack \u2014 ${PRESET_DESCRIPTIONS.fullstack}` }
|
|
379
|
+
]
|
|
380
|
+
});
|
|
381
|
+
const detected = await detectProviders2({ withVersion: true });
|
|
382
|
+
const provider = await select({
|
|
383
|
+
message: "Which agent?",
|
|
384
|
+
choices: detected.map((d) => ({
|
|
385
|
+
value: d.id,
|
|
386
|
+
name: d.status === "present" ? `${d.label} (${d.command}${d.version ? ` ${d.version}` : ""})` : `${d.label} (not installed)`,
|
|
387
|
+
disabled: false
|
|
388
|
+
}))
|
|
389
|
+
});
|
|
390
|
+
const spec = specForProvider2(provider);
|
|
391
|
+
if (!spec) {
|
|
392
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
393
|
+
}
|
|
394
|
+
const modelAnswer = await select({
|
|
395
|
+
message: "Team model preference:",
|
|
396
|
+
choices: [
|
|
397
|
+
{ value: "", name: "Role-based tier defaults (recommended)" },
|
|
398
|
+
new Separator("---"),
|
|
399
|
+
...[
|
|
400
|
+
{ label: "Strong (architect, devops)", tier: "strong" },
|
|
401
|
+
{ label: "Balanced (backend, frontend)", tier: "balanced" },
|
|
402
|
+
{ label: "Fast (reviewer, tester)", tier: "fast" }
|
|
403
|
+
].map((t) => {
|
|
404
|
+
const m = tierModel(spec, t.tier);
|
|
405
|
+
return m ? { value: m, name: `${t.label} \u2192 ${m}` } : void 0;
|
|
406
|
+
}).filter((x) => x !== void 0),
|
|
407
|
+
new Separator("---"),
|
|
408
|
+
...spec.models.map((m) => ({ value: m.id, name: m.label }))
|
|
409
|
+
]
|
|
410
|
+
});
|
|
411
|
+
const modelDefault = modelAnswer || void 0;
|
|
412
|
+
const overrideIds = await checkbox({
|
|
413
|
+
message: "Override model for specific agents? (space to select, enter to confirm)",
|
|
414
|
+
choices: overrideChoices(preset)
|
|
415
|
+
});
|
|
416
|
+
const overrides = [];
|
|
417
|
+
for (const id of overrideIds) {
|
|
418
|
+
const ov = await select({
|
|
419
|
+
message: `Model for ${id}:`.replace(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase()),
|
|
420
|
+
choices: spec.models.map((m) => ({ value: m.id, name: m.label }))
|
|
421
|
+
});
|
|
422
|
+
overrides.push({ id, model: ov });
|
|
423
|
+
}
|
|
424
|
+
const workDir = await input({
|
|
425
|
+
message: "Work directory (where agents read and write files):",
|
|
426
|
+
default: "."
|
|
427
|
+
});
|
|
428
|
+
try {
|
|
429
|
+
fs2.writeFileSync(
|
|
430
|
+
configPath,
|
|
431
|
+
generateConfig({ preset, workDir, projectName, provider, model: modelDefault, overrides })
|
|
432
|
+
);
|
|
433
|
+
if (workDir !== ".") {
|
|
434
|
+
fs2.mkdirSync(path2.resolve(process.cwd(), workDir), { recursive: true });
|
|
435
|
+
}
|
|
436
|
+
} catch (err) {
|
|
437
|
+
console.error(`\x1B[31mError: Failed to create files: ${err.message}\x1B[0m`);
|
|
438
|
+
process.exit(1);
|
|
439
|
+
}
|
|
440
|
+
const detectedProvider = detected.find((d) => d.id === provider);
|
|
441
|
+
console.log("");
|
|
442
|
+
console.log(`\x1B[32m\u2713\x1B[0m Created multiclaw.config.ts`);
|
|
443
|
+
console.log(` preset: ${preset} (${PRESET_DESCRIPTIONS[preset]})`);
|
|
444
|
+
console.log(` agent: ${spec.label}`);
|
|
445
|
+
if (detectedProvider?.status === "present") {
|
|
446
|
+
console.log(
|
|
447
|
+
` ${detectedProvider.command}${detectedProvider.version ? ` ${detectedProvider.version}` : ""}`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
console.log(` model: ${modelDefault || "role-based tier defaults"}`);
|
|
451
|
+
console.log(` workDir: ${workDir}`);
|
|
452
|
+
if (overrides.length > 0) {
|
|
453
|
+
console.log(` overrides: ${overrides.map((o) => `${o.id}=${o.model}`).join(", ")}`);
|
|
454
|
+
}
|
|
455
|
+
console.log("");
|
|
456
|
+
if (detectedProvider?.status === "missing") {
|
|
457
|
+
console.log(`\x1B[33m\u26A0 ${spec.label} is not installed.\x1B[0m`);
|
|
458
|
+
console.log(` ${spec.install.hint}`);
|
|
459
|
+
console.log("");
|
|
460
|
+
}
|
|
461
|
+
if (detectedProvider?.note) {
|
|
462
|
+
console.log(`\x1B[2m Before your first run: ${detectedProvider.note}\x1B[0m`);
|
|
463
|
+
console.log("");
|
|
464
|
+
}
|
|
465
|
+
console.log(`> Run: multiclaw (interactive console)`);
|
|
466
|
+
console.log(` multiclaw run "..." (headless)`);
|
|
467
|
+
console.log("");
|
|
468
|
+
console.log(
|
|
469
|
+
`\x1B[2mTip: the CLI resolves the "multiclaw" import itself, so the config runs either way.\x1B[0m`
|
|
470
|
+
);
|
|
471
|
+
console.log(
|
|
472
|
+
`\x1B[2m \`npm i -D multiclaw\` adds editor autocomplete for defineConfig/agents.\x1B[0m`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// src/commands/run.ts
|
|
477
|
+
import * as fs5 from "fs";
|
|
478
|
+
import * as path5 from "path";
|
|
479
|
+
import {
|
|
480
|
+
describeResumePlan,
|
|
481
|
+
Orchestrator as OrchestratorImpl
|
|
482
|
+
} from "@multiclawcli/core";
|
|
483
|
+
|
|
484
|
+
// src/lib.ts
|
|
485
|
+
import {
|
|
486
|
+
detectProviders as detectProviders3,
|
|
487
|
+
presentProviders as presentProviders2,
|
|
488
|
+
resolveCommand as resolveCommand2,
|
|
489
|
+
runtimeRegistry
|
|
490
|
+
} from "@multiclawcli/core";
|
|
491
|
+
import { agents, defineConfig } from "@multiclawcli/core";
|
|
492
|
+
async function assertRuntimesAvailable(config) {
|
|
493
|
+
const checked = /* @__PURE__ */ new Set();
|
|
494
|
+
for (const agent of config.agents) {
|
|
495
|
+
const name = agent.runtime ?? "acp";
|
|
496
|
+
if (!runtimeRegistry.has(name)) {
|
|
497
|
+
const supported = runtimeRegistry.list().join(", ");
|
|
498
|
+
console.error(`\x1B[31mError: Unknown runtime "${name}". Supported: ${supported}\x1B[0m`);
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
const command = resolveCommand2(agent, config);
|
|
502
|
+
const key = `${name}\0${command}`;
|
|
503
|
+
if (checked.has(key)) continue;
|
|
504
|
+
checked.add(key);
|
|
505
|
+
const info = await runtimeRegistry.get(name).describe({ command });
|
|
506
|
+
if (!info.available) {
|
|
507
|
+
const subject = `Command "${command}"`;
|
|
508
|
+
console.error(
|
|
509
|
+
`\x1B[31mError: ${subject} is not available. Make sure the CLI is installed.\x1B[0m`
|
|
510
|
+
);
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
51
516
|
// src/reporters/ConsoleReporter.ts
|
|
52
517
|
var isFancy = !process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY === true;
|
|
53
518
|
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: "*" };
|
|
@@ -70,6 +535,11 @@ var C = isFancy ? {
|
|
|
70
535
|
red: "",
|
|
71
536
|
purple: ""
|
|
72
537
|
};
|
|
538
|
+
function markFor(status) {
|
|
539
|
+
if (status === "success") return { icon: S.ok, color: C.green };
|
|
540
|
+
if (status === "cancelled") return { icon: S.skip, color: C.yellow };
|
|
541
|
+
return { icon: S.fail, color: C.red };
|
|
542
|
+
}
|
|
73
543
|
var ConsoleReporter = class {
|
|
74
544
|
attach(eventBus) {
|
|
75
545
|
return eventBus.subscribe((e) => this.handle(e));
|
|
@@ -83,8 +553,20 @@ var ConsoleReporter = class {
|
|
|
83
553
|
${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
|
|
84
554
|
console.log(`${edge[1]} ${S.run} ${e.payload.name.padEnd(46)}${edge[1]}`);
|
|
85
555
|
console.log(`${edge[2]}${line}${edge[4]}${C.reset}`);
|
|
86
|
-
console.log(
|
|
556
|
+
console.log(
|
|
557
|
+
`${C.cyan}${S.bullet} Agents: ${e.payload.totalAgents} Stages: ${e.payload.stages.length}${C.reset}
|
|
558
|
+
`
|
|
559
|
+
);
|
|
560
|
+
if (e.payload.selectionReason) {
|
|
561
|
+
console.log(`${C.dim}${S.bullet} Why: ${e.payload.selectionReason}${C.reset}
|
|
87
562
|
`);
|
|
563
|
+
}
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case "orchestration:stages": {
|
|
567
|
+
console.log(
|
|
568
|
+
`${C.dim}${S.bullet} Plan applied \u2014 ${e.payload.stages.length} stages${C.reset}`
|
|
569
|
+
);
|
|
88
570
|
break;
|
|
89
571
|
}
|
|
90
572
|
case "stage:start": {
|
|
@@ -97,6 +579,7 @@ ${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
|
|
|
97
579
|
break;
|
|
98
580
|
}
|
|
99
581
|
case "agent:output": {
|
|
582
|
+
if (e.payload.kind && e.payload.kind !== "text") break;
|
|
100
583
|
process.stdout.write(` ${e.payload.chunk}`);
|
|
101
584
|
break;
|
|
102
585
|
}
|
|
@@ -106,6 +589,10 @@ ${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
|
|
|
106
589
|
break;
|
|
107
590
|
}
|
|
108
591
|
case "agent:failed": {
|
|
592
|
+
if (e.payload.status === "cancelled") {
|
|
593
|
+
console.log(`${C.yellow}${S.skip} ${e.payload.agentName} cancelled${C.reset}`);
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
109
596
|
console.log(`${C.red}${S.fail} ${e.payload.agentName} failed: ${e.payload.error}${C.reset}`);
|
|
110
597
|
break;
|
|
111
598
|
}
|
|
@@ -114,26 +601,50 @@ ${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
|
|
|
114
601
|
break;
|
|
115
602
|
}
|
|
116
603
|
case "agent:retrying": {
|
|
117
|
-
console.log(
|
|
604
|
+
console.log(
|
|
605
|
+
`${C.yellow}${S.retry} ${e.payload.agentName} retry ${e.payload.attempt}/${e.payload.maxAttempts}: ${e.payload.error}${C.reset}`
|
|
606
|
+
);
|
|
607
|
+
break;
|
|
608
|
+
}
|
|
609
|
+
case "orchestration:warning": {
|
|
610
|
+
console.log(`${C.yellow}${S.warn} ${e.payload.message}${C.reset}`);
|
|
118
611
|
break;
|
|
119
612
|
}
|
|
120
613
|
case "orchestration:complete": {
|
|
121
|
-
const { success, totalDuration, agentResults } = e.payload;
|
|
614
|
+
const { success, totalDuration, agentResults, reply } = e.payload;
|
|
615
|
+
if (reply) {
|
|
616
|
+
const mark = reply.mode === "ask" ? "?" : S.ok;
|
|
617
|
+
console.log(`
|
|
618
|
+
${C.cyan}${mark} ${reply.message}${C.reset}`);
|
|
619
|
+
}
|
|
122
620
|
const line = (isFancy ? "\u2550" : "=").repeat(50);
|
|
123
621
|
const edge = isFancy ? ["\u2554", "\u2551", "\u255A", "\u2557", "\u255D"] : ["+", "|", "+", "+", "+"];
|
|
124
|
-
const title = success ? `${S.ok} All done` : `${S.warn} Done (with failures)`;
|
|
622
|
+
const title = reply ? reply.mode === "ask" ? `${S.warn} Needs your input` : `${S.ok} Answered` : e.payload.cancelled ? `${S.skip} Cancelled` : success ? `${S.ok} All done` : `${S.warn} Done (with failures)`;
|
|
125
623
|
console.log(`
|
|
126
624
|
${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
|
|
127
625
|
console.log(`${edge[1]} ${title.padEnd(48)}${edge[1]}`);
|
|
128
626
|
console.log(`${edge[2]}${line}${edge[4]}${C.reset}`);
|
|
129
627
|
const nameWidth = Math.max(8, ...agentResults.map((r) => r.agentName.length));
|
|
130
628
|
const totalStr = `${(totalDuration / 1e3).toFixed(1)}s`;
|
|
131
|
-
|
|
629
|
+
const cost = e.payload.totalCostUsd;
|
|
630
|
+
const costStr = cost === void 0 ? "" : ` $${cost.toFixed(2)}`;
|
|
631
|
+
console.log(` ~ ${"Total".padEnd(nameWidth)} ${totalStr.padStart(7)}${costStr}`);
|
|
132
632
|
for (const r of agentResults) {
|
|
133
|
-
const icon = r.status
|
|
134
|
-
const color = r.status === "success" ? C.green : C.red;
|
|
633
|
+
const { icon, color: color2 } = markFor(r.status);
|
|
135
634
|
const dur = `${(r.duration / 1e3).toFixed(1)}s`;
|
|
136
|
-
console.log(
|
|
635
|
+
console.log(
|
|
636
|
+
` ${color2}${icon}${C.reset} ${r.agentName.padEnd(nameWidth)} ${dur.padStart(7)}`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
if (e.payload.deliverables && e.payload.deliverables.length > 0) {
|
|
640
|
+
console.log(`
|
|
641
|
+
Deliverables:`);
|
|
642
|
+
for (const file of e.payload.deliverables.slice(0, 5)) {
|
|
643
|
+
console.log(` ${S.bullet} ${file.path} (${file.status}, ${file.size} bytes)`);
|
|
644
|
+
}
|
|
645
|
+
if (e.payload.deliverables.length > 5) {
|
|
646
|
+
console.log(` ${S.bullet} \u2026 ${e.payload.deliverables.length - 5} more`);
|
|
647
|
+
}
|
|
137
648
|
}
|
|
138
649
|
console.log();
|
|
139
650
|
break;
|
|
@@ -143,250 +654,280 @@ ${C.bold}${C.purple}${edge[0]}${line}${edge[3]}`);
|
|
|
143
654
|
};
|
|
144
655
|
|
|
145
656
|
// src/reporters/FileLogWriter.ts
|
|
146
|
-
import * as
|
|
147
|
-
import * as
|
|
657
|
+
import * as fs3 from "fs";
|
|
658
|
+
import * as path3 from "path";
|
|
148
659
|
var FileLogWriter = class {
|
|
149
|
-
constructor(logDir)
|
|
660
|
+
constructor(logDir, onError = (message) => process.stderr.write(`
|
|
661
|
+
[multiclaw] ${message}
|
|
662
|
+
`)) {
|
|
150
663
|
this.logDir = logDir;
|
|
664
|
+
this.onError = onError;
|
|
151
665
|
this.streams = /* @__PURE__ */ new Map();
|
|
152
|
-
|
|
666
|
+
/** Agent ids whose log could not be opened — never retried, so a broken
|
|
667
|
+
* directory does not produce one failing stream per output chunk. */
|
|
668
|
+
this.failed = /* @__PURE__ */ new Set();
|
|
669
|
+
fs3.mkdirSync(logDir, { recursive: true });
|
|
153
670
|
}
|
|
154
671
|
attach(eventBus) {
|
|
155
672
|
return eventBus.subscribe((e) => this.handle(e));
|
|
156
673
|
}
|
|
674
|
+
/**
|
|
675
|
+
* Flush every open stream.
|
|
676
|
+
*
|
|
677
|
+
* `stream.end()` only *starts* the flush, so a caller that exits the moment
|
|
678
|
+
* `orchestration:complete` arrives can lose the tail of the last agent's log.
|
|
679
|
+
* Awaiting this is what makes that tail durable.
|
|
680
|
+
*/
|
|
681
|
+
async drain() {
|
|
682
|
+
if (!this.closing) this.closing = this.flush();
|
|
683
|
+
await this.closing;
|
|
684
|
+
}
|
|
157
685
|
handle(e) {
|
|
158
686
|
if (e.type === "agent:output") {
|
|
159
|
-
|
|
687
|
+
const { agentId, chunk, kind } = e.payload;
|
|
688
|
+
if (kind && kind !== "text") return;
|
|
689
|
+
this.getStream(agentId)?.write(chunk);
|
|
160
690
|
}
|
|
161
691
|
if (e.type === "orchestration:complete") {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
692
|
+
try {
|
|
693
|
+
fs3.writeFileSync(path3.join(this.logDir, "report.json"), JSON.stringify(e.payload, null, 2));
|
|
694
|
+
} catch (err) {
|
|
695
|
+
this.onError(`Could not write report.json: ${errorMessage(err)}`);
|
|
696
|
+
}
|
|
697
|
+
this.closing ??= this.flush();
|
|
167
698
|
}
|
|
168
699
|
}
|
|
169
700
|
getStream(agentId) {
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
)
|
|
701
|
+
if (this.failed.has(agentId)) return void 0;
|
|
702
|
+
let stream = this.streams.get(agentId);
|
|
703
|
+
if (!stream) {
|
|
704
|
+
stream = fs3.createWriteStream(path3.join(this.logDir, `${agentId}.log`));
|
|
705
|
+
stream.on("error", (err) => {
|
|
706
|
+
this.failed.add(agentId);
|
|
707
|
+
this.streams.delete(agentId);
|
|
708
|
+
this.onError(`Could not write ${agentId}.log: ${errorMessage(err)}`);
|
|
709
|
+
});
|
|
710
|
+
this.streams.set(agentId, stream);
|
|
175
711
|
}
|
|
176
|
-
return
|
|
712
|
+
return stream;
|
|
177
713
|
}
|
|
178
|
-
|
|
179
|
-
|
|
714
|
+
flush() {
|
|
715
|
+
const streams = [...this.streams.values()];
|
|
180
716
|
this.streams.clear();
|
|
717
|
+
return Promise.all(
|
|
718
|
+
streams.map(
|
|
719
|
+
(s) => new Promise((resolve4) => {
|
|
720
|
+
if (s.destroyed || s.writableEnded) {
|
|
721
|
+
resolve4();
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
s.end(() => resolve4());
|
|
725
|
+
})
|
|
726
|
+
)
|
|
727
|
+
).then(() => void 0);
|
|
181
728
|
}
|
|
182
729
|
};
|
|
730
|
+
function errorMessage(err) {
|
|
731
|
+
return err instanceof Error ? err.message : String(err);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/commands/resume.ts
|
|
735
|
+
import * as fs4 from "fs";
|
|
736
|
+
import * as path4 from "path";
|
|
737
|
+
function resolveRunDir(runsRoot, target) {
|
|
738
|
+
if (typeof target !== "string" || target === "latest") {
|
|
739
|
+
return latestRunDir(runsRoot);
|
|
740
|
+
}
|
|
741
|
+
const resolved = path4.resolve(target);
|
|
742
|
+
const runDir = path4.basename(resolved) === "logs" ? path4.dirname(resolved) : resolved;
|
|
743
|
+
if (!fs4.existsSync(runDir)) {
|
|
744
|
+
throw new Error(`Run directory not found: ${runDir}`);
|
|
745
|
+
}
|
|
746
|
+
return runDir;
|
|
747
|
+
}
|
|
748
|
+
function latestRunDir(runsRoot) {
|
|
749
|
+
const names = fs4.existsSync(runsRoot) ? fs4.readdirSync(runsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory() && entry.name.startsWith("run-")).map((entry) => entry.name).sort() : [];
|
|
750
|
+
const latest = names[names.length - 1];
|
|
751
|
+
if (!latest) {
|
|
752
|
+
throw new Error(`No runs found to resume \u2014 looked in ${runsRoot}`);
|
|
753
|
+
}
|
|
754
|
+
return path4.join(runsRoot, latest);
|
|
755
|
+
}
|
|
756
|
+
function readReport(runDir) {
|
|
757
|
+
const reportPath = path4.join(runDir, "logs", "report.json");
|
|
758
|
+
let raw;
|
|
759
|
+
try {
|
|
760
|
+
raw = fs4.readFileSync(reportPath, "utf-8");
|
|
761
|
+
} catch {
|
|
762
|
+
throw new Error(`No report.json in ${runDir} \u2014 it may not be a complete run`);
|
|
763
|
+
}
|
|
764
|
+
try {
|
|
765
|
+
return JSON.parse(raw);
|
|
766
|
+
} catch (err) {
|
|
767
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
768
|
+
throw new Error(`Could not parse ${reportPath}: ${detail}`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function resumeOptionsFromReport(report, requirementOverride) {
|
|
772
|
+
const agents2 = [...new Set(report.agentResults.map((result) => result.agentId))];
|
|
773
|
+
if (agents2.length === 0) {
|
|
774
|
+
throw new Error("The previous run has no agent results \u2014 nothing to resume");
|
|
775
|
+
}
|
|
776
|
+
const sessions = {};
|
|
777
|
+
for (const result of report.agentResults) {
|
|
778
|
+
if (result.sessionId) sessions[result.agentId] = result.sessionId;
|
|
779
|
+
}
|
|
780
|
+
const requirement = requirementOverride ?? report.requirement;
|
|
781
|
+
if (requirement === void 0) {
|
|
782
|
+
throw new Error("The previous run recorded no requirement \u2014 pass one explicitly to resume");
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
requirement,
|
|
786
|
+
resume: {
|
|
787
|
+
agents: agents2,
|
|
788
|
+
// Absent, not `{}`: core reads a missing `sessions` as "start every agent
|
|
789
|
+
// fresh", which is exactly the single-agent retry of a failed agent.
|
|
790
|
+
...Object.keys(sessions).length > 0 ? { sessions } : {}
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
}
|
|
183
794
|
|
|
184
795
|
// src/commands/run.ts
|
|
185
|
-
|
|
186
|
-
|
|
796
|
+
var EXIT_CANCELLED = 130;
|
|
797
|
+
async function runCommand(requirementArg, options) {
|
|
798
|
+
const config = await loadConfig(options.config, requirementArg);
|
|
187
799
|
if (options.noLeader) {
|
|
188
800
|
config.useLeader = false;
|
|
189
801
|
}
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
802
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
803
|
+
if (options.resume !== void 0) {
|
|
804
|
+
const runsRoot = path5.join(config.workDir, ".multiclaw", "runs");
|
|
805
|
+
const runDir = resolveRunDir(runsRoot, options.resume);
|
|
806
|
+
const report = readReport(runDir);
|
|
807
|
+
const { requirement, resume } = resumeOptionsFromReport(report, requirementArg);
|
|
808
|
+
config.workDir = runDir;
|
|
809
|
+
config.resume = resume;
|
|
810
|
+
config.context = { ...config.context, requirement };
|
|
811
|
+
config.logDir = path5.join(runDir, `logs-resume-${stamp}`);
|
|
812
|
+
const plan = describeResumePlan({
|
|
813
|
+
agents: resume.agents,
|
|
814
|
+
sessions: resume.sessions,
|
|
815
|
+
team: config.agents
|
|
816
|
+
});
|
|
817
|
+
console.log(`\x1B[2mResuming ${runDir} (${resume.agents.length} agents) \u2014 ${plan}\x1B[0m`);
|
|
818
|
+
} else {
|
|
819
|
+
config.workDir = path5.join(config.workDir, ".multiclaw", "runs", `run-${stamp}`);
|
|
820
|
+
}
|
|
821
|
+
if (options.dryRun) {
|
|
822
|
+
const preview = await new OrchestratorImpl(config).preview();
|
|
823
|
+
printDryRun(preview, config.workDir);
|
|
824
|
+
process.exitCode = 0;
|
|
825
|
+
return;
|
|
205
826
|
}
|
|
827
|
+
await assertRuntimesAvailable(config);
|
|
828
|
+
const before = snapshotFiles(config.workDir);
|
|
206
829
|
const orchestrator = new OrchestratorImpl(config);
|
|
830
|
+
orchestrator.eventBus.subscribe((event) => {
|
|
831
|
+
if (event.type !== "orchestration:complete") return;
|
|
832
|
+
event.payload.deliverables = collectDeliverables(config.workDir, before);
|
|
833
|
+
});
|
|
207
834
|
new ConsoleReporter().attach(orchestrator.eventBus);
|
|
208
|
-
const logDir = config.logDir ??
|
|
209
|
-
new FileLogWriter(logDir)
|
|
835
|
+
const logDir = config.logDir ?? path5.join(config.workDir, "logs");
|
|
836
|
+
const logs = new FileLogWriter(logDir);
|
|
837
|
+
logs.attach(orchestrator.eventBus);
|
|
210
838
|
console.log(`\x1B[2mLogs: ${logDir}\x1B[0m`);
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
839
|
+
const controller = new AbortController();
|
|
840
|
+
const onSigint = () => {
|
|
841
|
+
if (controller.signal.aborted) process.exit(EXIT_CANCELLED);
|
|
842
|
+
controller.abort();
|
|
843
|
+
};
|
|
844
|
+
process.on("SIGINT", onSigint);
|
|
845
|
+
try {
|
|
846
|
+
const result = await orchestrator.run({ signal: controller.signal });
|
|
847
|
+
await logs.drain();
|
|
848
|
+
process.exitCode = result.cancelled ? EXIT_CANCELLED : result.success ? 0 : 1;
|
|
849
|
+
} finally {
|
|
850
|
+
process.off("SIGINT", onSigint);
|
|
218
851
|
}
|
|
219
|
-
const result = await orchestrator.run();
|
|
220
|
-
process.exit(result.success ? 0 : 1);
|
|
221
852
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
853
|
+
function printDryRun(preview, workDir) {
|
|
854
|
+
console.log("Dry run \u2014 no pipeline agents will execute.");
|
|
855
|
+
console.log(`Project: ${preview.name}`);
|
|
856
|
+
console.log(`Work dir: ${workDir}`);
|
|
857
|
+
if (preview.requirement) console.log(`Requirement: ${preview.requirement}`);
|
|
858
|
+
if (preview.reply) {
|
|
859
|
+
console.log(`${preview.reply.mode === "ask" ? "Question" : "Reply"}: ${preview.reply.message}`);
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
console.log(`Agents: ${preview.totalAgents}`);
|
|
863
|
+
if (preview.selectionReason) console.log(`Why: ${preview.selectionReason}`);
|
|
864
|
+
for (const stage of preview.stages) {
|
|
865
|
+
const agents2 = stage.agents.map(
|
|
866
|
+
(agent) => `${agent.id}${agent.model ? ` (${agent.model})` : ""}`
|
|
867
|
+
);
|
|
868
|
+
console.log(`Stage ${stage.stageIndex + 1}: ${agents2.join(", ") || "\u2014"}`);
|
|
233
869
|
}
|
|
234
870
|
}
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
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));
|
|
871
|
+
function snapshotFiles(root) {
|
|
872
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
873
|
+
walkFiles(root, (_absolute, relative2, stat) => {
|
|
874
|
+
snapshot.set(relative2, { mtimeMs: stat.mtimeMs, size: stat.size });
|
|
253
875
|
});
|
|
876
|
+
return snapshot;
|
|
254
877
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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: "."
|
|
878
|
+
function collectDeliverables(root, before) {
|
|
879
|
+
const deliverables = [];
|
|
880
|
+
walkFiles(root, (_absolute, relative2, stat) => {
|
|
881
|
+
const previous = before.get(relative2);
|
|
882
|
+
if (previous && previous.mtimeMs === stat.mtimeMs && previous.size === stat.size) return;
|
|
883
|
+
deliverables.push({
|
|
884
|
+
path: relative2,
|
|
885
|
+
status: previous ? "modified" : "created",
|
|
886
|
+
size: stat.size,
|
|
887
|
+
modifiedAt: stat.mtime.toISOString()
|
|
888
|
+
});
|
|
332
889
|
});
|
|
333
|
-
|
|
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();
|
|
890
|
+
return deliverables.sort((a, b) => a.path.localeCompare(b.path)).slice(0, 20);
|
|
349
891
|
}
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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`);
|
|
892
|
+
function walkFiles(root, visit) {
|
|
893
|
+
if (!fs5.existsSync(root)) return;
|
|
894
|
+
const stack = [root];
|
|
895
|
+
while (stack.length > 0) {
|
|
896
|
+
const dir = stack.pop();
|
|
897
|
+
for (const name of fs5.readdirSync(dir)) {
|
|
898
|
+
if (name === ".multiclaw") continue;
|
|
899
|
+
const absolute = path5.join(dir, name);
|
|
900
|
+
const stat = fs5.statSync(absolute);
|
|
901
|
+
if (stat.isDirectory()) stack.push(absolute);
|
|
902
|
+
else if (stat.isFile()) visit(absolute, path5.relative(root, absolute), stat);
|
|
365
903
|
}
|
|
366
904
|
}
|
|
367
905
|
}
|
|
368
906
|
|
|
369
|
-
// src/commands/
|
|
370
|
-
async function
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
await
|
|
375
|
-
|
|
376
|
-
|
|
907
|
+
// src/commands/tui.ts
|
|
908
|
+
async function tuiCommand(options) {
|
|
909
|
+
const config = await loadConfig(options.config);
|
|
910
|
+
await assertRuntimesAvailable(config);
|
|
911
|
+
const { startTui } = await import("@multiclawcli/dashboard/tui");
|
|
912
|
+
await startTui({
|
|
913
|
+
config,
|
|
914
|
+
onTurnStart: (eventBus, logDir) => {
|
|
915
|
+
new FileLogWriter(logDir).attach(eventBus);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
377
918
|
}
|
|
378
919
|
|
|
379
920
|
// src/commands/upgrade.ts
|
|
380
|
-
import { execSync
|
|
381
|
-
import { createRequire } from "module";
|
|
382
|
-
var require2 =
|
|
921
|
+
import { execSync } from "child_process";
|
|
922
|
+
import { createRequire as createRequire2 } from "module";
|
|
923
|
+
var require2 = createRequire2(import.meta.url);
|
|
383
924
|
async function upgradeCommand() {
|
|
384
925
|
const { version: currentVersion } = require2("../package.json");
|
|
385
926
|
console.log(`Current version: v${currentVersion}`);
|
|
386
927
|
console.log("Checking for updates...");
|
|
387
928
|
let latestVersion;
|
|
388
929
|
try {
|
|
389
|
-
latestVersion =
|
|
930
|
+
latestVersion = execSync("npm show multiclaw version", { encoding: "utf8" }).trim();
|
|
390
931
|
} catch {
|
|
391
932
|
console.error("\x1B[31mError: Failed to fetch latest version from npm registry\x1B[0m");
|
|
392
933
|
process.exit(1);
|
|
@@ -398,25 +939,31 @@ async function upgradeCommand() {
|
|
|
398
939
|
}
|
|
399
940
|
console.log(`Upgrading multiclaw v${currentVersion} \u2192 v${latestVersion}...`);
|
|
400
941
|
try {
|
|
401
|
-
|
|
942
|
+
execSync("npm install -g multiclaw@latest", { stdio: "inherit" });
|
|
402
943
|
console.log(`\x1B[32m\u2713\x1B[0m Successfully upgraded to v${latestVersion}`);
|
|
403
944
|
} catch {
|
|
404
|
-
console.error(
|
|
945
|
+
console.error(
|
|
946
|
+
"\x1B[31mError: Upgrade failed. Try running manually: npm install -g multiclaw@latest\x1B[0m"
|
|
947
|
+
);
|
|
405
948
|
process.exit(1);
|
|
406
949
|
}
|
|
407
950
|
}
|
|
408
951
|
|
|
409
952
|
// src/index.ts
|
|
410
953
|
var __filename2 = fileURLToPath(import.meta.url);
|
|
411
|
-
var __dirname2 =
|
|
412
|
-
var pkg = JSON.parse(
|
|
954
|
+
var __dirname2 = dirname3(__filename2);
|
|
955
|
+
var pkg = JSON.parse(readFileSync2(join4(__dirname2, "../package.json"), "utf-8"));
|
|
413
956
|
var program = new Command();
|
|
414
|
-
program.name("multiclaw").description("Multi-agent orchestration CLI").version(pkg.version, "-v, --version", "Display version number");
|
|
957
|
+
program.name("multiclaw").description("Multi-agent orchestration CLI").version(pkg.version, "-v, --version", "Display version number").action((options) => tuiCommand(options));
|
|
415
958
|
program.command("init").description("Scaffold a config file (interactive)").action(() => initCommand());
|
|
416
|
-
program.command("run
|
|
417
|
-
|
|
959
|
+
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").option("--dry-run", "Preview the routed plan without running the pipeline").option(
|
|
960
|
+
"--resume [path]",
|
|
961
|
+
"Resume a previous run, continuing agent sessions (default: the latest)"
|
|
962
|
+
).action((requirement, opts) => runCommand(requirement, opts));
|
|
418
963
|
program.command("upgrade").description("Upgrade multiclaw to the latest version").action(() => upgradeCommand());
|
|
964
|
+
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
965
|
program.parseAsync(process.argv).catch((err) => {
|
|
420
|
-
|
|
966
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
967
|
+
console.error(`\x1B[31mError: ${message}\x1B[0m`);
|
|
421
968
|
process.exit(1);
|
|
422
969
|
});
|