multiclaw 0.3.2 → 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/README.md +192 -50
- package/dist/index.js +531 -228
- package/dist/lib.d.ts +21 -1
- package/dist/lib.js +40 -2
- package/package.json +14 -10
package/README.md
CHANGED
|
@@ -9,13 +9,21 @@ Multi-agent orchestration CLI. Define a pipeline of AI agents, give them a requi
|
|
|
9
9
|
|
|
10
10
|
## Prerequisites
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
multiclaw orchestrates agents from any installed **provider**. Every provider talks
|
|
13
|
+
to multiclaw over the same [Agent Client Protocol (ACP)](https://agentclientprotocol.com)
|
|
14
|
+
— the underlying *runtime* is always `acp`; the **provider** is the agent product you
|
|
15
|
+
choose. Install one or more:
|
|
16
|
+
|
|
17
|
+
| Provider | CLI | Install |
|
|
18
|
+
|----------|-----|---------|
|
|
19
|
+
| OpenCode | `opencode` | `npm install -g opencode` |
|
|
20
|
+
| Claude | `claude-agent-acp` (bundled) | install the Claude Code CLI (`claude`) and run `claude` to log in |
|
|
21
|
+
| Codex | `codex-acp` (bundled) | install the Codex CLI (`codex`) and run `codex login` |
|
|
22
|
+
| Cursor | `cursor-agent` | `curl https://cursor.com/install \| bash` |
|
|
23
|
+
|
|
24
|
+
`multiclaw init` auto-detects which providers are installed and only offers those;
|
|
25
|
+
`multiclaw doctor` shows the full picture (installed versions, auth hints, available
|
|
26
|
+
models). Codex and Cursor need an API key or login before they can run.
|
|
19
27
|
|
|
20
28
|
## Install
|
|
21
29
|
|
|
@@ -29,11 +37,15 @@ npm install -g multiclaw
|
|
|
29
37
|
# 1. Scaffold a config in the current directory
|
|
30
38
|
multiclaw init
|
|
31
39
|
|
|
32
|
-
# 2.
|
|
33
|
-
multiclaw
|
|
40
|
+
# 2. Open the interactive console
|
|
41
|
+
multiclaw
|
|
42
|
+
|
|
43
|
+
# ...or run a single orchestration headlessly
|
|
44
|
+
multiclaw run "Build a REST API for a todo list"
|
|
34
45
|
```
|
|
35
46
|
|
|
36
|
-
|
|
47
|
+
`multiclaw` with no subcommand opens a full-screen terminal console. Use `multiclaw run` for
|
|
48
|
+
headless execution (CI, piped output).
|
|
37
49
|
|
|
38
50
|
## Commands
|
|
39
51
|
|
|
@@ -45,6 +57,20 @@ Prompts:
|
|
|
45
57
|
- **Project name** — defaults to the current directory name
|
|
46
58
|
- **Preset** — pipeline template (see [Presets](#presets))
|
|
47
59
|
- **Work directory** — where agents read and write files
|
|
60
|
+
- **Provider** — which agent product to use (`claude`, `opencode`, `codex`, `cursor`). Only installed providers are offered; missing ones are listed with install hints.
|
|
61
|
+
- **Model preference** — role-based tier defaults (recommended) or a specific tier/model
|
|
62
|
+
- **Per-agent overrides** (optional) — pick individual agents and set a model for each
|
|
63
|
+
|
|
64
|
+
### `multiclaw doctor`
|
|
65
|
+
|
|
66
|
+
Diagnose the local environment: which providers are installed (with versions), what auth
|
|
67
|
+
each needs, how many models each exposes, and whether the current config's agents all
|
|
68
|
+
resolve to an installed provider. Exits non-zero if no provider is available.
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
multiclaw doctor # full environment check
|
|
72
|
+
multiclaw doctor --config ./multiclaw.config.ts # also validates the config
|
|
73
|
+
```
|
|
48
74
|
|
|
49
75
|
### `multiclaw run <requirement> [options]`
|
|
50
76
|
|
|
@@ -53,24 +79,11 @@ Runs the orchestration defined in your config file.
|
|
|
53
79
|
| Option | Description |
|
|
54
80
|
|--------|-------------|
|
|
55
81
|
| `--config <path>` | Path to config file. Auto-discovers `multiclaw.config.ts` if omitted. |
|
|
56
|
-
| `--
|
|
57
|
-
| `--port <port>` | Dashboard port. Requires `--ui`. Default: `3210`. |
|
|
58
|
-
| `--server-url <url>` | Stream events to a running `multiclaw serve` instance. |
|
|
59
|
-
| `--no-leader` | Skip the leader agent and use the default pipeline directly. |
|
|
82
|
+
| `--no-leader` | (Batch mode only) Skip the leader and run the full configured team. Has no effect in the interactive console — the leader is always used there. |
|
|
60
83
|
|
|
61
84
|
The `<requirement>` string is injected into every agent's prompt as `{{requirement}}`.
|
|
62
85
|
|
|
63
|
-
Each run creates an isolated timestamped subdirectory inside `workDir` (e.g. `workspace/run-2026-10-08T12-00-00`).
|
|
64
|
-
|
|
65
|
-
### `multiclaw serve [options]`
|
|
66
|
-
|
|
67
|
-
Start a persistent Dashboard server. Useful for CI or when you want a permanent URL to monitor runs.
|
|
68
|
-
|
|
69
|
-
| Option | Description |
|
|
70
|
-
|--------|-------------|
|
|
71
|
-
| `--port <port>` | Port to listen on. Default: `3210`. |
|
|
72
|
-
|
|
73
|
-
Connect a run to it with `multiclaw run "..." --server-url http://localhost:3210`.
|
|
86
|
+
Each run creates an isolated timestamped subdirectory inside `workDir` (e.g. `workspace/.multiclaw/runs/run-2026-10-08T12-00-00`).
|
|
74
87
|
|
|
75
88
|
## Config file
|
|
76
89
|
|
|
@@ -96,18 +109,21 @@ export default defineConfig({
|
|
|
96
109
|
continueOnError: false, // optional; keep running if an agent fails
|
|
97
110
|
maxConcurrency: 4, // optional; max agents running at once
|
|
98
111
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
112
|
+
// Project-wide defaults applied to every agent that doesn't override them.
|
|
113
|
+
// One line replaces repeating provider/model across all preset agents.
|
|
114
|
+
defaults: {
|
|
115
|
+
provider: "claude", // which agent product runs the team
|
|
116
|
+
model: "sonnet", // default model (overridable per agent)
|
|
102
117
|
},
|
|
103
118
|
|
|
104
119
|
agents: [
|
|
105
120
|
{
|
|
106
121
|
id: "architect", // unique identifier, used in dependsOn
|
|
107
122
|
name: "Architect", // display name
|
|
108
|
-
icon: "◆", // optional; shown in
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
icon: "◆", // optional; shown in the console
|
|
124
|
+
provider: "claude", // optional; default: config.defaults.provider
|
|
125
|
+
modelTier: "strong", // optional; "strong" | "balanced" | "fast"
|
|
126
|
+
model: "opus", // optional; overrides tier/default for this agent
|
|
111
127
|
systemPrompt: "You are a software architect.", // optional
|
|
112
128
|
taskPrompt: `Design the system for {{requirement}}.
|
|
113
129
|
Read the brief at {{file:brief.md}} and write architecture.md.`,
|
|
@@ -133,30 +149,32 @@ Read the brief at {{file:brief.md}} and write architecture.md.`,
|
|
|
133
149
|
|
|
134
150
|
| Field | Type | Required | Description |
|
|
135
151
|
|-------|------|----------|-------------|
|
|
136
|
-
| `name` | string | ✓ | Project name, shown in the
|
|
137
|
-
| `workDir` | string | ✓ | Base directory where agents read and write files. |
|
|
152
|
+
| `name` | string | ✓ | Project name, shown in the console header. |
|
|
153
|
+
| `workDir` | string | ✓ | Base directory where agents read and write files. Every console turn shares it. |
|
|
138
154
|
| `agents` | AgentDefinition[] | ✓ | The agent pipeline (see Agent fields below). |
|
|
139
155
|
| `leader` | AgentDefinition | | Optional routing agent (see [Leader](#leader)). |
|
|
140
156
|
| `useLeader` | boolean | | Enable or disable the leader agent. Default: `true` when leader is configured. |
|
|
157
|
+
| `defaults` | object | | Project-wide `{ provider?, model?, command? }` applied to every agent that does not override them. Keeps generated configs short. |
|
|
141
158
|
| `context` | Record<string, string> | | Variables available as `{{key}}` in every prompt. `requirement` is injected automatically. |
|
|
142
159
|
| `logDir` | string | | Log output directory. Default: `workDir/.multiclaw/logs`. |
|
|
143
160
|
| `continueOnError` | boolean | | Keep running when an agent fails. Default: `false`. |
|
|
144
161
|
| `maxConcurrency` | number | | Max agents running in parallel. Default: unlimited. |
|
|
145
|
-
| `dashboard.port` | number | | Dashboard port. Default: `3210`. |
|
|
146
|
-
| `dashboard.autoOpen` | boolean | | Open the dashboard in a browser automatically. Default: `true`. |
|
|
147
162
|
|
|
148
163
|
### Agent fields
|
|
149
164
|
|
|
150
165
|
| Field | Type | Required | Description |
|
|
151
166
|
|-------|------|----------|-------------|
|
|
152
167
|
| `id` | string | ✓ | Unique identifier. Used in `dependsOn`. |
|
|
153
|
-
| `name` | string | ✓ | Display name shown in
|
|
168
|
+
| `name` | string | ✓ | Display name shown in the agent list. |
|
|
154
169
|
| `taskPrompt` | string | ✓ | Prompt sent to the AI runtime. Supports template variables. |
|
|
155
170
|
| `systemPrompt` | string | | Optional system prompt prepended to `taskPrompt`. |
|
|
156
171
|
| `icon` | string | | Unicode icon shown next to the agent name. |
|
|
157
|
-
| `
|
|
158
|
-
| `
|
|
159
|
-
| `
|
|
172
|
+
| `description` | string | | One-line role, shown beside the agent's name on the console's first screen. Every built-in preset sets one. |
|
|
173
|
+
| `provider` | string | | Agent product this agent runs on: `claude`, `opencode`, `codex`, or `cursor`. Defaults to `config.defaults.provider`. All providers speak the same `acp` runtime. |
|
|
174
|
+
| `runtime` | string | | **Legacy.** Always `"acp"` today. Use `provider` instead. Retained for backward compatibility. |
|
|
175
|
+
| `modelTier` | `"strong"` \| `"balanced"` \| `"fast"` | | Declares the capability this agent needs; the catalog maps each tier to a provider-specific model id. Ignored when `model` is set. |
|
|
176
|
+
| `model` | string | | Override the model id for this agent (provider-specific, e.g. `"opus"`, `"sonnet"`, `"haiku"` for Claude). Takes precedence over `modelTier` and `models`. |
|
|
177
|
+
| `models` | Record<string, string> | | Map of provider id (or the legacy `"acp"`) to a model id, e.g. `{ claude: "sonnet", opencode: "opencode/big-pickle" }`. Used when `model`/`modelTier` are unset. |
|
|
160
178
|
| `tools` | string[] | | Allowed tools. Default: `["Read", "Write", "Bash"]`. |
|
|
161
179
|
| `dependsOn` | string[] | | Agent ids that must complete before this agent starts. |
|
|
162
180
|
| `timeout` | number | | Per-agent timeout in ms. Default: `600000` (10 min). |
|
|
@@ -184,16 +202,122 @@ Stage 3: backend-developer │ ui-designer ← parallel
|
|
|
184
202
|
Stage 4: code-reviewer │ devops ← parallel
|
|
185
203
|
```
|
|
186
204
|
|
|
187
|
-
##
|
|
205
|
+
## Interactive console
|
|
188
206
|
|
|
189
|
-
|
|
207
|
+
`multiclaw` with no subcommand opens a full-screen terminal console. It is a **turn-based
|
|
208
|
+
conductor loop**, not a chat: each turn you type is one complete orchestration run of the whole
|
|
209
|
+
pipeline, and the console stays open so you can run the next one.
|
|
190
210
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
211
|
+
```
|
|
212
|
+
◆ multiclaw · dev-team turn 2 · 00:12
|
|
213
|
+
────────────────────────────────────────────────────────────────────────────
|
|
214
|
+
✓ ◆ Leader ──▸ ① ⠹ ◇ Architect ──▸ ② ○ ◇ Developer ──▸ ③ ○ ◇ Reviewer
|
|
215
|
+
|
|
216
|
+
TEAM 1/4 │ Architect · sonnet-4 · ⠹ running 4.2s ▼
|
|
217
|
+
✓ ◆ Leader 3.1s │ Read packages/core/src/orchestrator/…
|
|
218
|
+
› ⠹ ◇ Architect 4.2s │ Grep "withTimeout" in packages/core/src
|
|
219
|
+
○ ◇ Developer — │ Edit utils/timeout.ts — abort-safe
|
|
220
|
+
○ ◇ Reviewer — │ Bash pnpm -r test
|
|
221
|
+
|
|
222
|
+
» also add tests
|
|
223
|
+
› type to queue a follow-up…
|
|
224
|
+
⠹ running 4.2s · 1/4 agents · 1 queued enter queues · ctrl+c cancel
|
|
225
|
+
```
|
|
195
226
|
|
|
196
|
-
|
|
227
|
+
| Region | Shows |
|
|
228
|
+
|--------|-------|
|
|
229
|
+
| Header | Project name, turn number, and the phase or elapsed time |
|
|
230
|
+
| Pipeline rail | The leader, then each stage in order, joined by `──▸` |
|
|
231
|
+
| Team panel | One row per agent: status glyph, name, live time, and progress (`1/4`) |
|
|
232
|
+
| Stream panel | Live output of the focused agent, with a `▼` live / `⏸` parked marker |
|
|
233
|
+
| Reply panel | What the leader answered, when a turn needed no pipeline |
|
|
234
|
+
| Review banner | `viewing turn N …` while a past turn is open |
|
|
235
|
+
| Queue strip | Follow-ups typed mid-run, in the order they will run |
|
|
236
|
+
| Prompt | Where you type — live even while a turn is running |
|
|
237
|
+
| Hint bar | Aggregate status on the left, the keys that work *right now* on the right |
|
|
238
|
+
|
|
239
|
+
Status glyphs: `○` queued · `◐` running (a spinner while live) · `↻` retrying · `✓` done ·
|
|
240
|
+
`✗` failed · `⊘` skipped.
|
|
241
|
+
|
|
242
|
+
**First run.** Before the first turn the body shows the team from your config — each agent with the
|
|
243
|
+
`description` of what it is for — rather than an empty pane. `tab` moves the `›` caret down the list;
|
|
244
|
+
once a turn is running the caret follows whichever agent is speaking.
|
|
245
|
+
|
|
246
|
+
**Not every turn needs the team.** The leader decides that first. Ask it a question, or say hello,
|
|
247
|
+
and it answers in prose: the body swaps to the reply, the footer reads `✓ answered`, and no agent
|
|
248
|
+
runs. When the request is a real task but too vague to plan, the leader asks instead — the footer
|
|
249
|
+
reads `? needs your input` and your next line is the answer.
|
|
250
|
+
|
|
251
|
+
### Keys
|
|
252
|
+
|
|
253
|
+
The prompt always holds focus, so exactly two keys are reserved: `?` and `/`, and only while the
|
|
254
|
+
line is empty *and* the console is idle. Everything else is a named key or an ordinary character,
|
|
255
|
+
which is what keeps typing a requirement from tripping over shortcuts.
|
|
256
|
+
|
|
257
|
+
| Key | Action |
|
|
258
|
+
|-----|--------|
|
|
259
|
+
| `enter` | Run the typed requirement — or queue it while a turn is running |
|
|
260
|
+
| `tab` / `shift+tab` | Cycle the focused agent |
|
|
261
|
+
| `↑` `↓` `pgup` `pgdn` | Scroll the focused agent's stream |
|
|
262
|
+
| `←` `→` `home` `end` | Move the caret |
|
|
263
|
+
| `esc` | Stop reading a past turn, clear the line, or close an overlay |
|
|
264
|
+
| `?` | Key map (idle, empty line only) |
|
|
265
|
+
| `/` | Command menu (empty line only) |
|
|
266
|
+
| `ctrl+l` | Clear the focused agent's log |
|
|
267
|
+
| `ctrl+c` | Cancel the running turn *and its queue*; again when idle to quit |
|
|
268
|
+
|
|
269
|
+
Scrolling up parks the stream and scrolling back to the bottom resumes it, so there is no follow
|
|
270
|
+
toggle to remember. While a turn runs the pane follows whichever agent starts next, until you cycle
|
|
271
|
+
the focus by hand.
|
|
272
|
+
|
|
273
|
+
**Typing while the team works.** The prompt stays live. Anything you submit mid-run joins the queue
|
|
274
|
+
strip above it and starts as the next turn the moment the console goes idle — so a thought you have
|
|
275
|
+
while watching the output is never lost. Cancelling drops the queue with the turn, since "cancel"
|
|
276
|
+
should mean stop.
|
|
277
|
+
|
|
278
|
+
Cancelling aborts the in-flight agents and their child processes rather than leaving them running in
|
|
279
|
+
the background.
|
|
280
|
+
|
|
281
|
+
### Commands
|
|
282
|
+
|
|
283
|
+
Type `/` on an empty line for a filtered list.
|
|
284
|
+
|
|
285
|
+
| Command | Action |
|
|
286
|
+
|---------|--------|
|
|
287
|
+
| `/agents` | List the configured team |
|
|
288
|
+
| `/focus <id>` | Focus an agent by id |
|
|
289
|
+
| `/clear` | Clear the focused agent's log |
|
|
290
|
+
| `/history` | Browse finished turns |
|
|
291
|
+
| `/cancel` | Cancel the running turn and its queue |
|
|
292
|
+
| `/help` | Key map |
|
|
293
|
+
| `/quit` | Exit |
|
|
294
|
+
|
|
295
|
+
The console renders on the alternate screen buffer, so your shell scrollback is preserved.
|
|
296
|
+
|
|
297
|
+
### Workspace
|
|
298
|
+
|
|
299
|
+
The console is iterative, so every turn runs against the **same** `workDir` — a follow-up like "now
|
|
300
|
+
add tests for that" sees the files the previous turn wrote. `task-plan.json` is cleared at the start
|
|
301
|
+
of each turn so a plan left over from an earlier requirement can never be re-applied. Only the logs
|
|
302
|
+
are per-run: `workDir/.multiclaw/runs/run-<timestamp>/logs/`.
|
|
303
|
+
|
|
304
|
+
`multiclaw run` is deliberately different: it sandboxes each invocation in its own
|
|
305
|
+
`run-<timestamp>/` directory.
|
|
306
|
+
|
|
307
|
+
### History
|
|
308
|
+
|
|
309
|
+
`/history` lists finished turns newest first, with each turn's outcome, duration, agent count and
|
|
310
|
+
requirement. `↑` `↓` select and `enter` opens one for reading — the panes render that turn behind a
|
|
311
|
+
`viewing turn N` banner, and `esc` returns to the live turn. Scrolling and `tab` apply to what you
|
|
312
|
+
are reading, never to the turn that is still running. The turn on screen is not listed, and the last
|
|
313
|
+
20 finished turns are kept.
|
|
314
|
+
|
|
315
|
+
On a narrow terminal (under 72 columns) the team panel is dropped and the stream takes the full
|
|
316
|
+
width; on a short one (under 16 rows) the pipeline rail goes too. The header, prompt and hint bar are
|
|
317
|
+
never dropped.
|
|
318
|
+
|
|
319
|
+
> **No TTY?** The console needs an interactive terminal. In CI, in a pipe, or over a non-TTY SSH
|
|
320
|
+
> session it exits with a message pointing at `multiclaw run`, which runs the same pipeline headless.
|
|
197
321
|
|
|
198
322
|
## Logs
|
|
199
323
|
|
|
@@ -229,7 +353,24 @@ Presets are pipeline templates you can pick during `multiclaw init`:
|
|
|
229
353
|
|
|
230
354
|
## Leader
|
|
231
355
|
|
|
232
|
-
The `leader` field adds a routing agent that runs **before** the main pipeline. It reads the requirement and inspects the codebase, then decides
|
|
356
|
+
The `leader` field adds a routing agent that runs **before** the main pipeline. It reads the requirement and inspects the codebase, then decides what the turn actually needs:
|
|
357
|
+
|
|
358
|
+
| Decision | Meaning |
|
|
359
|
+
|----------|---------|
|
|
360
|
+
| `run` | It is a task the leader can scope — name the minimum set of agents needed |
|
|
361
|
+
| `reply` | A greeting or a question it can answer on the spot; no agent runs |
|
|
362
|
+
| `ask` | A real task, but too vague to plan; it asks the one question that unblocks it |
|
|
363
|
+
|
|
364
|
+
The leader is asked for a single JSON object:
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
{"mode": "run", "run": ["backend-developer", "code-reviewer"]}
|
|
368
|
+
{"mode": "reply", "message": "Hi — ask me for a change to this project."}
|
|
369
|
+
{"mode": "ask", "message": "Which database should I target?"}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
A bare `{"run": [...]}` with no `mode` is still accepted as a run. Anything else the leader writes is
|
|
373
|
+
treated as its answer, so a leader that replies in prose still ends the turn in words.
|
|
233
374
|
|
|
234
375
|
```typescript
|
|
235
376
|
export default defineConfig({
|
|
@@ -243,9 +384,9 @@ export default defineConfig({
|
|
|
243
384
|
})
|
|
244
385
|
```
|
|
245
386
|
|
|
246
|
-
For a minor bug fix the leader might decide only `backend-developer` and `code-reviewer` are needed, skipping the design stages. For a greenfield feature it runs the full pipeline.
|
|
387
|
+
For a minor bug fix the leader might decide only `backend-developer` and `code-reviewer` are needed, skipping the design stages. For a greenfield feature it runs the full pipeline. For `hello` it answers, and nobody is spent on it.
|
|
247
388
|
|
|
248
|
-
If the leader
|
|
389
|
+
Agent IDs are always a **subset** of the configured team: an invented id is reported and dropped, never added. A `run` naming no configured agent is a failure, not a green "done". If the leader cannot be reached at all, multiclaw warns and falls back to the full configured team.
|
|
249
390
|
|
|
250
391
|
### Disabling the leader
|
|
251
392
|
|
|
@@ -319,7 +460,8 @@ See [`examples/dev-team/`](https://github.com/shipengqi/multiclaw/tree/main/exam
|
|
|
319
460
|
|
|
320
461
|
```bash
|
|
321
462
|
cd examples/dev-team
|
|
322
|
-
pnpm dev
|
|
463
|
+
pnpm dev # opens the interactive console
|
|
464
|
+
pnpm run run # ...or one headless run
|
|
323
465
|
```
|
|
324
466
|
|
|
325
467
|
## License
|
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/
|
|
10
|
-
import
|
|
11
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
546
|
+
const color2 = r.status === "success" ? C.green : C.red;
|
|
135
547
|
const dur = `${(r.duration / 1e3).toFixed(1)}s`;
|
|
136
|
-
console.log(
|
|
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
|
|
147
|
-
import * as
|
|
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
|
-
|
|
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)
|
|
590
|
+
this.getStream(e.payload.agentId)?.write(e.payload.chunk);
|
|
160
591
|
}
|
|
161
592
|
if (e.type === "orchestration:complete") {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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 (
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
|
613
|
+
return stream;
|
|
177
614
|
}
|
|
178
|
-
|
|
179
|
-
|
|
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 =
|
|
192
|
-
|
|
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 ??
|
|
209
|
-
new FileLogWriter(logDir)
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
|
381
|
-
import { createRequire } from "module";
|
|
382
|
-
var require2 =
|
|
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
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 =
|
|
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
|
-
|
|
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(
|
|
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("--
|
|
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
|
-
|
|
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
|
});
|
package/dist/lib.d.ts
CHANGED
|
@@ -1 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
import { MultiClawConfig } from '@multiclawcli/core';
|
|
2
|
+
export { AgentDefinition, MultiClawConfig, agents, defineConfig, detectProviders, presentProviders } from '@multiclawcli/core';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hard gate for `run`/`tui`: every runtime the config names must exist and
|
|
6
|
+
* report itself as installed, or the process exits before any work starts.
|
|
7
|
+
*
|
|
8
|
+
* Availability comes from `describe()` rather than an ad-hoc `execSync`, so
|
|
9
|
+
* "is this runtime installed?" has exactly one implementation. `describe()`
|
|
10
|
+
* receives the agent's configured command because an external-process runtime
|
|
11
|
+
* (e.g. ACP) is only as available as the binary it was told to launch.
|
|
12
|
+
*/
|
|
13
|
+
declare function assertRuntimesAvailable(config: MultiClawConfig): Promise<void>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Legacy alias: returns runtime names that have at least one provider present.
|
|
17
|
+
* Prefer `detectProviders()` for new code.
|
|
18
|
+
*/
|
|
19
|
+
declare function availableRuntimes(): Promise<string[]>;
|
|
20
|
+
|
|
21
|
+
export { assertRuntimesAvailable, availableRuntimes };
|
package/dist/lib.js
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
// src/lib.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
detectProviders,
|
|
4
|
+
presentProviders,
|
|
5
|
+
resolveCommand,
|
|
6
|
+
runtimeRegistry
|
|
7
|
+
} from "@multiclawcli/core";
|
|
8
|
+
import { agents, defineConfig } from "@multiclawcli/core";
|
|
9
|
+
async function assertRuntimesAvailable(config) {
|
|
10
|
+
const checked = /* @__PURE__ */ new Set();
|
|
11
|
+
for (const agent of config.agents) {
|
|
12
|
+
const name = agent.runtime ?? "acp";
|
|
13
|
+
if (!runtimeRegistry.has(name)) {
|
|
14
|
+
const supported = runtimeRegistry.list().join(", ");
|
|
15
|
+
console.error(`\x1B[31mError: Unknown runtime "${name}". Supported: ${supported}\x1B[0m`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const command = resolveCommand(agent, config);
|
|
19
|
+
const key = `${name}\0${command}`;
|
|
20
|
+
if (checked.has(key)) continue;
|
|
21
|
+
checked.add(key);
|
|
22
|
+
const info = await runtimeRegistry.get(name).describe({ command });
|
|
23
|
+
if (!info.available) {
|
|
24
|
+
const subject = `Command "${command}"`;
|
|
25
|
+
console.error(
|
|
26
|
+
`\x1B[31mError: ${subject} is not available. Make sure the CLI is installed.\x1B[0m`
|
|
27
|
+
);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function availableRuntimes() {
|
|
33
|
+
const detected = await detectProviders();
|
|
34
|
+
const present = presentProviders(detected);
|
|
35
|
+
return present.length > 0 ? ["acp"] : [];
|
|
36
|
+
}
|
|
3
37
|
export {
|
|
4
38
|
agents,
|
|
5
|
-
|
|
39
|
+
assertRuntimesAvailable,
|
|
40
|
+
availableRuntimes,
|
|
41
|
+
defineConfig,
|
|
42
|
+
detectProviders,
|
|
43
|
+
presentProviders
|
|
6
44
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "multiclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"multiclaw": "dist/index.js"
|
|
@@ -27,21 +27,25 @@
|
|
|
27
27
|
"url": "https://github.com/shipengqi/multiclaw/issues"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@agentclientprotocol/claude-agent-acp": "^0.89.1",
|
|
31
|
+
"@agentclientprotocol/codex-acp": "^2.2.2",
|
|
30
32
|
"@inquirer/prompts": "^8.7.3",
|
|
31
|
-
"@multiclawcli/core": "0.
|
|
32
|
-
"@multiclawcli/dashboard": "0.
|
|
33
|
-
"commander": "^
|
|
34
|
-
"
|
|
35
|
-
"tsx": "^4.7.0",
|
|
36
|
-
"ws": "^8.16.0"
|
|
33
|
+
"@multiclawcli/core": "0.4.0",
|
|
34
|
+
"@multiclawcli/dashboard": "0.4.0",
|
|
35
|
+
"commander": "^15.0.0",
|
|
36
|
+
"jiti": "^2.7.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@types/node": "^
|
|
40
|
-
"
|
|
41
|
-
"typescript": "^5.4.0"
|
|
39
|
+
"@types/node": "^26.6.4",
|
|
40
|
+
"tsx": "^4.7.0",
|
|
41
|
+
"typescript": "^5.4.0",
|
|
42
|
+
"vitest": "^4.1.11"
|
|
42
43
|
},
|
|
43
44
|
"scripts": {
|
|
44
45
|
"build": "tsup",
|
|
46
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"test:coverage": "vitest run --coverage",
|
|
45
49
|
"dev": "tsx src/index.ts"
|
|
46
50
|
}
|
|
47
51
|
}
|