461 lines
12 KiB
Markdown
461 lines
12 KiB
Markdown
# ai-cli-dispatch
|
||
|
||
Dispatch AI CLI coding tasks to available clients (Codex, Claude Code, OpenCode) with automatic discovery, version checking, and execution.
|
||
|
||
## Scope
|
||
|
||
- discover installed AI CLI clients on the host
|
||
- report client versions and availability
|
||
- dispatch a prompt to a specific client by name
|
||
- auto-resolve the best client from prompt keywords
|
||
- forward arguments natively to each client
|
||
- run tasks asynchronously as background jobs with lifecycle management
|
||
- run tasks synchronously when blocking until completion is desired
|
||
|
||
The tool supports both async (default) and sync execution modes. Async jobs run as detached background processes and are tracked on disk. For ACP-based harnesses, see `docs/openclaw-acp-orchestration.md`.
|
||
|
||
## Setup
|
||
|
||
From the repo or installed skill directory:
|
||
|
||
```bash
|
||
cd tools/ai-cli-dispatch
|
||
npm install
|
||
```
|
||
|
||
The dispatcher itself requires only Node.js 20+ and `npm`. The actual AI CLI clients (`codex`, `claude`, `opencode`) are discovered from the host `PATH`; they are not bundled.
|
||
|
||
## Commands
|
||
|
||
```bash
|
||
ai-cli-dispatch list [--json|--text]
|
||
ai-cli-dispatch run --client <client> --prompt <prompt> [--sync] [--timeout <ms>] [--debug] [--json|--text]
|
||
ai-cli-dispatch dispatch <prompt> [--client <client>] [--sync] [--timeout <ms>] [--debug] [--json|--text]
|
||
ai-cli-dispatch start --client <client> --prompt <prompt> [--timeout <ms>] [--debug] [--json|--text]
|
||
ai-cli-dispatch status <job-id> [--json|--text]
|
||
ai-cli-dispatch results <job-id> [--json|--text]
|
||
ai-cli-dispatch cancel <job-id> [--json|--text]
|
||
ai-cli-dispatch list-jobs [--status running|completed|failed] [--json|--text]
|
||
ai-cli-dispatch cleanup-jobs [--max-age <number>[h|m|s|d]] [--json|--text]
|
||
ai-cli-dispatch --help
|
||
```
|
||
|
||
### `list`
|
||
|
||
Discover and report all supported clients.
|
||
|
||
```bash
|
||
ai-cli-dispatch list --json
|
||
```
|
||
|
||
Example JSON output:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"name": "codex",
|
||
"path": "/usr/local/bin/codex",
|
||
"version": "1.2.3",
|
||
"found": true
|
||
},
|
||
{
|
||
"name": "claude",
|
||
"found": false
|
||
},
|
||
{
|
||
"name": "opencode",
|
||
"path": "/opt/homebrew/bin/opencode",
|
||
"version": "0.5.1",
|
||
"found": true
|
||
}
|
||
]
|
||
```
|
||
|
||
Use `--text` for human-readable checkmarks:
|
||
|
||
```bash
|
||
ai-cli-dispatch list --text
|
||
```
|
||
|
||
### `run`
|
||
|
||
Execute a prompt directly through a named client. By default, this starts a background job and returns immediately.
|
||
|
||
```bash
|
||
# Async (default) — returns a job ID
|
||
ai-cli-dispatch run --client codex --prompt "refactor this function"
|
||
|
||
# Sync — blocks until the client finishes
|
||
ai-cli-dispatch run --client claude --prompt "add tests for auth middleware" --sync
|
||
|
||
# With custom timeout and debug diagnostics
|
||
ai-cli-dispatch run --client opencode --prompt "migrate to ESM" --timeout 600000 --debug
|
||
```
|
||
|
||
The prompt is forwarded with each client’s native argument shape:
|
||
|
||
| Client | Arguments passed |
|
||
|---|---|
|
||
| `codex` | `exec --yolo "<prompt>"` |
|
||
| `claude` | `-p "<prompt>" --dangerously-skip-permissions` |
|
||
| `opencode` | `run --dangerously-skip-permissions "<prompt>"` |
|
||
|
||
### `dispatch`
|
||
|
||
Auto-resolve the client from prompt keywords, then execute. Defaults to async; use `--sync` to block.
|
||
|
||
```bash
|
||
ai-cli-dispatch dispatch "use claude to write tests"
|
||
ai-cli-dispatch dispatch "codex refactor auth module"
|
||
ai-cli-dispatch dispatch "opencode migrate to ESM"
|
||
```
|
||
|
||
Keyword matching is case-insensitive and ordered:
|
||
|
||
1. `--client` flag (highest precedence)
|
||
2. `"open code"` (spaced variant) → `opencode`
|
||
3. `"claude"` → `claude`
|
||
4. `"codex"` → `codex`
|
||
5. `"opencode"` → `opencode`
|
||
6. `defaultClient` from config (lowest precedence)
|
||
|
||
Override auto-resolution explicitly:
|
||
|
||
```bash
|
||
ai-cli-dispatch dispatch "fix the bug" --client claude
|
||
```
|
||
|
||
### `start`
|
||
|
||
Explicitly start a background job (same as `run` without `--sync`). Useful when you want the async behavior unambiguously.
|
||
|
||
```bash
|
||
ai-cli-dispatch start --client codex --prompt "refactor this function"
|
||
```
|
||
|
||
### `status`
|
||
|
||
Check the status of a background job.
|
||
|
||
```bash
|
||
ai-cli-dispatch status <job-id>
|
||
```
|
||
|
||
JSON output:
|
||
|
||
```json
|
||
{
|
||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||
"client": "codex",
|
||
"prompt": "refactor this function",
|
||
"status": "running",
|
||
"startedAt": "2026-05-19T12:34:56.789Z",
|
||
"pid": 12345
|
||
}
|
||
```
|
||
|
||
Statuses: `running`, `completed`, `failed`, `timed_out`, `cancelled`.
|
||
|
||
### `results`
|
||
|
||
Retrieve the execution result of a completed job.
|
||
|
||
```bash
|
||
ai-cli-dispatch results <job-id>
|
||
```
|
||
|
||
JSON output:
|
||
|
||
```json
|
||
{
|
||
"stdout": "...",
|
||
"stderr": "...",
|
||
"exitCode": 0,
|
||
"client": "codex",
|
||
"durationMs": 45231
|
||
}
|
||
```
|
||
|
||
Requires status `completed`. For `failed` or `timed_out` jobs, use `status` to see the captured error.
|
||
|
||
### `cancel`
|
||
|
||
Cancel a running job.
|
||
|
||
```bash
|
||
ai-cli-dispatch cancel <job-id>
|
||
```
|
||
|
||
### `list-jobs`
|
||
|
||
List all tracked jobs, newest first.
|
||
|
||
```bash
|
||
ai-cli-dispatch list-jobs --json
|
||
ai-cli-dispatch list-jobs --status running --json
|
||
```
|
||
|
||
### `cleanup-jobs`
|
||
|
||
Remove job files older than a threshold. Default unit is hours.
|
||
|
||
```bash
|
||
ai-cli-dispatch cleanup-jobs --max-age 24h
|
||
ai-cli-dispatch cleanup-jobs --max-age 30m
|
||
```
|
||
|
||
## Async vs Sync Mode
|
||
|
||
By default, `run` and `dispatch` are **async**: they start a detached background process, persist a job record to disk, and return a job ID immediately. This is ideal for:
|
||
|
||
- Fire-and-forget tasks that may run for minutes
|
||
- Long-running codegen or migration tasks
|
||
- Scenarios where the caller should not block
|
||
|
||
Use `--sync` when you need:
|
||
|
||
- The complete output before the next step
|
||
- Synchronous composition in shell pipelines or scripts
|
||
- Immediate error propagation to the calling process
|
||
|
||
| Aspect | Async (default) | Sync (`--sync`) |
|
||
|---|---|---|
|
||
| Return value | Job ID + status | Full stdout/stderr + exit code |
|
||
| Process model | Detached child, parent exits immediately | Attached child, parent waits |
|
||
| Persistence | Job file written to disk | No job file |
|
||
| Timeout | Enforced via `child.kill()` after `--timeout` | Enforced via `child.kill()` after `--timeout` |
|
||
|
||
## Client Discovery
|
||
|
||
Discovery searches `PATH` in this order for each client name:
|
||
|
||
1. `codex` — OpenAI Codex CLI
|
||
2. `claude` — Anthropic Claude Code
|
||
3. `opencode` — OpenCode CLI
|
||
|
||
The search uses `which` (or `where` on Windows) first, then falls back to a manual `PATH` directory scan. If a binary is found, `--version` is invoked to extract a semver string.
|
||
|
||
## Configuration
|
||
|
||
Optional config file:
|
||
|
||
```text
|
||
~/.openclaw/ai-cli-dispatch.json
|
||
```
|
||
|
||
Example:
|
||
|
||
```json
|
||
{
|
||
"paths": {
|
||
"codex": "/usr/local/bin/codex",
|
||
"claude": "/opt/homebrew/bin/claude"
|
||
},
|
||
"defaultClient": "claude",
|
||
"timeout": 300000
|
||
}
|
||
```
|
||
|
||
Resolution priority for paths and default client (highest to lowest):
|
||
|
||
1. CLI flag (`--client`, `--codex-path`, etc.)
|
||
2. Environment variable (`AI_CLI_CODEX_PATH`, `AI_CLI_DEFAULT_CLIENT`, etc.)
|
||
3. Config file (`paths`, `defaultClient`)
|
||
4. `which` / `where` discovery
|
||
|
||
Supported env vars:
|
||
|
||
| Variable | Purpose |
|
||
|---|---|
|
||
| `AI_CLI_CODEX_PATH` | Override `codex` binary path |
|
||
| `AI_CLI_CLAUDE_PATH` | Override `claude` binary path |
|
||
| `AI_CLI_OPENCODE_PATH` | Override `opencode` binary path |
|
||
| `AI_CLI_DEFAULT_CLIENT` | Override default client (`codex`, `claude`, or `opencode`) |
|
||
|
||
## Output Model
|
||
|
||
Default output is JSON. Use `--text` to stream raw `stdout`/`stderr` directly.
|
||
|
||
### Sync JSON success shape (`run --sync`, `dispatch --sync`)
|
||
|
||
```json
|
||
{
|
||
"stdout": "...",
|
||
"stderr": "...",
|
||
"exitCode": 0,
|
||
"client": "codex",
|
||
"durationMs": 45231
|
||
}
|
||
```
|
||
|
||
### Async JSON success shape (`run`, `dispatch`, `start`)
|
||
|
||
```json
|
||
{
|
||
"jobId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||
"client": "codex",
|
||
"status": "running"
|
||
}
|
||
```
|
||
|
||
### Job status shape (`status`)
|
||
|
||
```json
|
||
{
|
||
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||
"client": "codex",
|
||
"prompt": "refactor this function",
|
||
"status": "running",
|
||
"startedAt": "2026-05-19T12:34:56.789Z",
|
||
"pid": 12345
|
||
}
|
||
```
|
||
|
||
### Job result shape (`results`)
|
||
|
||
```json
|
||
{
|
||
"stdout": "...",
|
||
"stderr": "...",
|
||
"exitCode": 0,
|
||
"client": "codex",
|
||
"durationMs": 45231
|
||
}
|
||
```
|
||
|
||
### JSON error shape
|
||
|
||
```json
|
||
{
|
||
"error": "..."
|
||
}
|
||
```
|
||
|
||
Exit codes:
|
||
|
||
| Code | Meaning |
|
||
|---|---|
|
||
| `0` | Success |
|
||
| `1` | Missing/unknown command, missing argument, unknown client, resolution failure, execution error, or job lifecycle error |
|
||
|
||
## Error Handling Guidance
|
||
|
||
### `Client "<name>" not found or not installed`
|
||
|
||
Meaning: the requested client binary is not on `PATH` and not overridden by config.
|
||
|
||
Actions:
|
||
|
||
1. Confirm the client is installed (`codex --version`, `claude --version`, etc.)
|
||
2. Check that its directory is on `PATH`
|
||
3. Or override the path in `~/.openclaw/ai-cli-dispatch.json`
|
||
|
||
### `Prompt cannot be empty`
|
||
|
||
Meaning: the prompt string was empty or whitespace-only.
|
||
|
||
Action: supply a non-empty `--prompt` or positional prompt argument.
|
||
|
||
### `Execution timed out after 600000ms`
|
||
|
||
Meaning: the client subprocess did not finish within the timeout.
|
||
|
||
Action: the client may be waiting for interactive input or the task is too large. Break the prompt into smaller pieces, increase `--timeout`, or run the client directly to diagnose. Async jobs that time out are recorded with status `timed_out`.
|
||
|
||
### `Could not resolve client from prompt`
|
||
|
||
Meaning: `dispatch` found no matching keyword and no `defaultClient` is configured.
|
||
|
||
Action: include a client name in the prompt (e.g., `"use claude to ..."`) or set `defaultClient` in config.
|
||
|
||
### `Job "<job-id>" not found`
|
||
|
||
Meaning: the requested job ID does not exist in the job store.
|
||
|
||
Action: verify the job ID. Job files are stored under `~/.openclaw/ai-cli-dispatch/jobs/`. If the directory was cleaned or the host restarted, old jobs may have been removed.
|
||
|
||
### `Job "<job-id>" result is not available (status: <status>)`
|
||
|
||
Meaning: `results` was called on a job that has not finished (`running`) or finished unsuccessfully (`failed`, `timed_out`, `cancelled`).
|
||
|
||
Action: poll `status` until the job reaches `completed`, or inspect `status` output for the error field.
|
||
|
||
## Job Lifecycle Workflows
|
||
|
||
### Fire-and-forget
|
||
|
||
```bash
|
||
JOB=$(ai-cli-dispatch run --client codex --prompt "refactor auth" --json | jq -r '.jobId')
|
||
# caller continues immediately
|
||
```
|
||
|
||
### Poll until completion
|
||
|
||
```bash
|
||
JOB=$(ai-cli-dispatch start --client claude --prompt "write tests" --json | jq -r '.jobId')
|
||
while [ "$(ai-cli-dispatch status "$JOB" --json | jq -r '.status')" = "running" ]; do
|
||
sleep 5
|
||
done
|
||
ai-cli-dispatch results "$JOB" --json
|
||
```
|
||
|
||
### Sync one-shot
|
||
|
||
```bash
|
||
ai-cli-dispatch run --client opencode --prompt "fix lint" --sync --text
|
||
```
|
||
|
||
### Batch cleanup
|
||
|
||
```bash
|
||
ai-cli-dispatch cleanup-jobs --max-age 24h
|
||
```
|
||
|
||
## Common Flows
|
||
|
||
### Check what is installed
|
||
|
||
```bash
|
||
ai-cli-dispatch list --json
|
||
```
|
||
|
||
### Run a quick task through a specific client (async)
|
||
|
||
```bash
|
||
ai-cli-dispatch run --client codex --prompt "fix lint errors in src/app.ts"
|
||
```
|
||
|
||
### Run a quick task synchronously
|
||
|
||
```bash
|
||
ai-cli-dispatch run --client codex --prompt "fix lint errors in src/app.ts" --sync
|
||
```
|
||
|
||
### Let the tool pick the client from the prompt
|
||
|
||
```bash
|
||
ai-cli-dispatch dispatch "claude: add unit tests for utils.ts"
|
||
```
|
||
|
||
### Force a client when the prompt is ambiguous
|
||
|
||
```bash
|
||
ai-cli-dispatch dispatch "review this PR" --client claude
|
||
```
|
||
|
||
## Coexistence with ACP
|
||
|
||
`ai-cli-dispatch` is a direct subprocess dispatcher. It is not an ACP agent and does not participate in ACP orchestration.
|
||
|
||
- Use `ai-cli-dispatch` when you need a quick, local, one-shot CLI execution or a background job.
|
||
- Use ACP (`docs/openclaw-acp-orchestration.md`) when you need session-bound coding harnesses with thread context, multi-turn review, or orchestrator-managed verification gates.
|
||
|
||
## Implementation Notes
|
||
|
||
- The dispatcher is TypeScript/Node.js with a single external dependency (`minimist`).
|
||
- Client arguments are hardcoded per tool to match each client’s stable CLI contract.
|
||
- The default timeout is 10 minutes (`600_000` ms); override with `--timeout` or config.
|
||
- On Windows, discovery uses `where` instead of `which` and `.exe` extensions are assumed.
|
||
- Async jobs run as detached processes with `stdio: ["ignore", "pipe", "pipe"]` so the dispatcher can exit without waiting.
|
||
- Job files are written atomically to `~/.openclaw/ai-cli-dispatch/jobs/<jobId>.json`.
|