merge M6 into implement/2026-05-18-stef-openclaw-skills
This commit is contained in:
@@ -11,6 +11,8 @@ This repository contains practical OpenClaw skills and companion integrations. I
|
|||||||
- Per-skill runtime instructions: `skills/<skill-name>/SKILL.md`
|
- Per-skill runtime instructions: `skills/<skill-name>/SKILL.md`
|
||||||
- Integration implementation files: `integrations/<integration-name>/`
|
- Integration implementation files: `integrations/<integration-name>/`
|
||||||
- Integration docs: `docs/*.md`
|
- Integration docs: `docs/*.md`
|
||||||
|
- Tool implementation files: `tools/<tool-name>/`
|
||||||
|
- Tool docs: `docs/*.md`
|
||||||
|
|
||||||
## Skills
|
## Skills
|
||||||
|
|
||||||
@@ -34,6 +36,12 @@ This repository contains practical OpenClaw skills and companion integrations. I
|
|||||||
| `google-maps` | Traffic-aware ETA and leave-by calculations using Google Maps APIs. | `integrations/google-maps` |
|
| `google-maps` | Traffic-aware ETA and leave-by calculations using Google Maps APIs. | `integrations/google-maps` |
|
||||||
| `google-workspace` | Gmail and Google Calendar helper CLI for profile, mail, attachment-capable send, calendar search, and event creation. | `integrations/google-workspace` |
|
| `google-workspace` | Gmail and Google Calendar helper CLI for profile, mail, attachment-capable send, calendar search, and event creation. | `integrations/google-workspace` |
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
| Tool | What it does | Path |
|
||||||
|
|---|---|---|
|
||||||
|
| `ai-cli-dispatch` | Dispatch AI CLI coding tasks to available clients (Codex, Claude Code, OpenCode) with automatic discovery, version checking, and execution. | `tools/ai-cli-dispatch` |
|
||||||
|
|
||||||
## Operator docs
|
## Operator docs
|
||||||
|
|
||||||
| Doc | What it covers |
|
| Doc | What it covers |
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ This folder contains detailed docs for each skill in this repository.
|
|||||||
- [`google-maps`](google-maps.md) — Traffic-aware ETA and leave-by calculations via Google Maps APIs
|
- [`google-maps`](google-maps.md) — Traffic-aware ETA and leave-by calculations via Google Maps APIs
|
||||||
- [`google-workspace`](google-workspace.md) — Gmail and Google Calendar helper CLI with attachment-capable send support
|
- [`google-workspace`](google-workspace.md) — Gmail and Google Calendar helper CLI with attachment-capable send support
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
- [`ai-cli-dispatch`](ai-cli-dispatch.md) — Dispatch AI CLI coding tasks to available clients with automatic discovery, version checking, and execution
|
||||||
|
- [`installation`](installation.md) — Prerequisites, install steps, PATH configuration, and optional config file setup for `ai-cli-dispatch`
|
||||||
|
- [`architecture`](architecture.md) — Design decisions, module breakdown, data flow, coexistence with ACP, and extension points for `ai-cli-dispatch`
|
||||||
|
|
||||||
## Operator Docs
|
## Operator Docs
|
||||||
|
|
||||||
- [`openclaw-acp-orchestration`](openclaw-acp-orchestration.md) — OpenClaw ACP orchestration for Codex and Claude Code on the gateway host
|
- [`openclaw-acp-orchestration`](openclaw-acp-orchestration.md) — OpenClaw ACP orchestration for Codex and Claude Code on the gateway host
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
The tool is a lightweight sync-only dispatcher. It does not implement streaming, chat sessions, or ACP orchestration. 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> [--json|--text]
|
||||||
|
ai-cli-dispatch dispatch <prompt> [--client <client>] [--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.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ai-cli-dispatch run --client codex --prompt "refactor this function"
|
||||||
|
ai-cli-dispatch run --client claude --prompt "add tests for auth middleware"
|
||||||
|
ai-cli-dispatch run --client opencode --prompt "migrate to ESM"
|
||||||
|
```
|
||||||
|
|
||||||
|
The prompt is forwarded with each client’s native argument shape:
|
||||||
|
|
||||||
|
| Client | Arguments passed |
|
||||||
|
|---|---|
|
||||||
|
| `codex` | `exec "<prompt>"` |
|
||||||
|
| `claude` | `-p "<prompt>"` |
|
||||||
|
| `opencode` | `"<prompt>"` |
|
||||||
|
|
||||||
|
### `dispatch`
|
||||||
|
|
||||||
|
Auto-resolve the client from prompt keywords, then execute.
|
||||||
|
|
||||||
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
JSON success shape (`run` and `dispatch`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"stdout": "...",
|
||||||
|
"stderr": "...",
|
||||||
|
"exitCode": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
JSON error shape:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit codes:
|
||||||
|
|
||||||
|
| Code | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `0` | Success |
|
||||||
|
| `1` | Missing/unknown command, missing argument, unknown client, resolution failure, or execution 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 300000ms`
|
||||||
|
|
||||||
|
Meaning: the client subprocess did not finish within the default 5-minute timeout.
|
||||||
|
|
||||||
|
Action: the client may be waiting for interactive input or the task is too large. Break the prompt into smaller pieces, or run the client directly to diagnose.
|
||||||
|
|
||||||
|
### `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.
|
||||||
|
|
||||||
|
## Common Flows
|
||||||
|
|
||||||
|
### Check what is installed
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ai-cli-dispatch list --json
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run a quick task through a specific client
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ai-cli-dispatch run --client codex --prompt "fix lint errors in src/app.ts"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 runs the client binary synchronously and returns its output. 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.
|
||||||
|
- 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 5 minutes (`300_000` ms).
|
||||||
|
- On Windows, discovery uses `where` instead of `which` and `.exe` extensions are assumed.
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
# ai-cli-dispatch Architecture
|
||||||
|
|
||||||
|
This document describes the internal design of `ai-cli-dispatch`, the module breakdown, data flow, key design decisions, and how to extend the tool.
|
||||||
|
|
||||||
|
## Module Breakdown
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/
|
||||||
|
├── cli.ts — Entry point: argument parsing, command routing, I/O formatting
|
||||||
|
├── types.ts — Shared types and error classes
|
||||||
|
├── constants.ts — Client name registry and platform helpers
|
||||||
|
├── config.ts — Layered configuration resolution (flags → env → file → PATH)
|
||||||
|
├── detect.ts — Client discovery: binary lookup and version extraction
|
||||||
|
├── dispatch.ts — Prompt-to-client resolution (explicit flag → keywords → default)
|
||||||
|
└── execute.ts — Subprocess spawning, stdout/stderr capture, timeout handling
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responsibilities
|
||||||
|
|
||||||
|
| Module | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `cli.ts` | Parses `argv` with `minimist`, routes to `list` / `run` / `dispatch`, prints JSON or text output, and controls the process exit code. |
|
||||||
|
| `types.ts` | Defines `ClientName`, `ClientInfo`, `ExecResult`, `ToolConfig`, and the error hierarchy (`ClientNotFoundError`, `ExecError`). |
|
||||||
|
| `constants.ts` | Holds the canonical `CLIENT_NAMES` array and `isWindows()` helper used by discovery and config. |
|
||||||
|
| `config.ts` | Resolves per-client binary paths and the optional `defaultClient` from four layered sources. |
|
||||||
|
| `detect.ts` | Locates each client binary on `PATH`, falls back to a manual directory scan, and invokes `--version` to extract a semver string. |
|
||||||
|
| `dispatch.ts` | Chooses the target client from a prompt string using ordered keyword matching, with overrides for explicit `--client` and `defaultClient`. |
|
||||||
|
| `execute.ts` | Spawns the chosen client with its native argument shape, buffers `stdout`/`stderr`, enforces a timeout, and returns an `ExecResult` or throws a typed error. |
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
A typical `dispatch` invocation flows through four stages:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||||
|
│ detect │ ──► │ config │ ──► │ dispatch │ ──► │ execute │
|
||||||
|
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
||||||
|
│ │ │ │
|
||||||
|
which/where flags/env/file keyword scan spawn child
|
||||||
|
PATH walk defaultClient --client override capture output
|
||||||
|
--version fallback default timeout / exitCode
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1. Detect
|
||||||
|
|
||||||
|
`detectClients()` iterates over `CLIENT_NAMES` and attempts to locate each binary:
|
||||||
|
|
||||||
|
1. Invoke `which <name>` (or `where <name>` on Windows).
|
||||||
|
2. If that fails, walk `PATH` segments manually and test `existsSync()`.
|
||||||
|
3. If a binary is found, run `<binary> --version` and parse the first semver-like match.
|
||||||
|
|
||||||
|
Result: an array of `ClientInfo` objects with `name`, `found`, `path`, and `version`.
|
||||||
|
|
||||||
|
### 2. Config
|
||||||
|
|
||||||
|
`resolveConfig()` builds a `ResolvedConfig` by layering sources (highest to lowest precedence):
|
||||||
|
|
||||||
|
1. **CLI flags** — `--codex-path`, `--claude-path`, `--opencode-path`, `--default-client`
|
||||||
|
2. **Environment variables** — `AI_CLI_CODEX_PATH`, `AI_CLI_CLAUDE_PATH`, `AI_CLI_OPENCODE_PATH`, `AI_CLI_DEFAULT_CLIENT`
|
||||||
|
3. **Config file** — `~/.openclaw/ai-cli-dispatch.json` (`paths` and `defaultClient` keys)
|
||||||
|
4. **PATH discovery** — `which`/`where` fallback via `defaultWhichSync()`
|
||||||
|
|
||||||
|
Only values for the three known `ClientName` entries are accepted; unknown `defaultClient` values are ignored.
|
||||||
|
|
||||||
|
### 3. Dispatch
|
||||||
|
|
||||||
|
`resolveClient(prompt, config)` decides which client to use:
|
||||||
|
|
||||||
|
1. If `config.client` is a valid `ClientName`, return it immediately.
|
||||||
|
2. Lower-case the prompt and scan for substrings in order:
|
||||||
|
- `"open code"` → `opencode`
|
||||||
|
- `"claude"` → `claude`
|
||||||
|
- `"codex"` → `codex`
|
||||||
|
- `"opencode"` → `opencode`
|
||||||
|
3. If no keyword matches, return `config.defaultClient` or `null`.
|
||||||
|
|
||||||
|
This ordering intentionally prioritizes `"open code"` before `"opencode"` so the spaced natural-language variant wins.
|
||||||
|
|
||||||
|
### 4. Execute
|
||||||
|
|
||||||
|
`executePrompt(client, prompt, options)` runs the selected client:
|
||||||
|
|
||||||
|
1. Reject empty or whitespace-only prompts with `ExecError`.
|
||||||
|
2. Validate that an explicit `clientPath` exists on disk (if provided).
|
||||||
|
3. Map the client to its native argument array via `CLIENT_ARGS`:
|
||||||
|
- `codex` → `["exec", prompt]`
|
||||||
|
- `claude` → `["-p", prompt]`
|
||||||
|
- `opencode` → `[prompt]`
|
||||||
|
4. `spawn()` the process with `shell: false`.
|
||||||
|
5. Buffer `stdout` and `stderr` via `"data"` listeners.
|
||||||
|
6. Start a `setTimeout`; if it fires, `child.kill()` is sent.
|
||||||
|
7. On `close`, resolve with `{ stdout, stderr, exitCode }`.
|
||||||
|
8. On `error`, reject with `ClientNotFoundError` for `ENOENT` or `ExecError` for anything else.
|
||||||
|
9. On timeout, reject with `ExecError` containing the buffered output so far.
|
||||||
|
|
||||||
|
The default timeout is **5 minutes** (`300_000` ms).
|
||||||
|
|
||||||
|
## Design Decisions
|
||||||
|
|
||||||
|
### Coexistence with ACP
|
||||||
|
|
||||||
|
`ai-cli-dispatch` is intentionally **not** an ACP agent. It is a thin, local subprocess wrapper with no session state, no thread binding, and no orchestrator protocol.
|
||||||
|
|
||||||
|
- Use `ai-cli-dispatch` when you need a quick, one-shot CLI execution on the gateway host.
|
||||||
|
- Use ACP (`docs/openclaw-acp-orchestration.md`) when you need session-bound coding harnesses, multi-turn review, or orchestrator-managed verification gates.
|
||||||
|
|
||||||
|
This separation keeps the dispatcher small and avoids duplicating ACP’s scheduling, context persistence, and review-loop responsibilities.
|
||||||
|
|
||||||
|
### Keyword Dispatch vs NLP
|
||||||
|
|
||||||
|
Client resolution uses deterministic substring matching instead of natural-language parsing or an LLM call.
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- **Speed:** No network round-trip or model load; resolution is synchronous and sub-millisecond.
|
||||||
|
- **Predictability:** The same prompt always resolves to the same client. There is no temperature, context window, or model-version drift.
|
||||||
|
- **Debuggability:** A user can read the ordered keyword list and know exactly why a given prompt resolved to a given client.
|
||||||
|
- **Scope fit:** The dispatcher only needs to distinguish three clients. A full NLP pipeline would be overkill.
|
||||||
|
|
||||||
|
The trade-off is that prompts like `"compare codex and claude"` resolve to `codex` because `"codex"` is checked first. Users can always override with `--client`.
|
||||||
|
|
||||||
|
### Sync-Only Initial Release
|
||||||
|
|
||||||
|
The current implementation is entirely synchronous from the caller’s perspective: `executePrompt` returns a promise that resolves only when the child process exits or the timeout fires.
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- The primary use case is one-shot tasks (refactor, add tests, migrate) where the agent needs the complete output before proceeding.
|
||||||
|
- Streaming would require a different output contract (callbacks, generators, or an event emitter) and complicates the JSON error model.
|
||||||
|
- ACP already covers interactive, streaming, and session-based use cases.
|
||||||
|
|
||||||
|
Streaming is an intentional future extension point (see below).
|
||||||
|
|
||||||
|
### Error Taxonomy
|
||||||
|
|
||||||
|
All runtime failures are represented as typed errors so callers and tests can branch precisely:
|
||||||
|
|
||||||
|
| Error | When thrown | Data carried |
|
||||||
|
|---|---|---|
|
||||||
|
| `ClientNotFoundError` | Binary not on `PATH`, explicit `clientPath` missing, or `ENOENT` from `spawn` | `message` with client name |
|
||||||
|
| `ExecError` | Empty prompt, unknown client, timeout, non-`ENOENT` spawn error, or child exit | `message` + full `ExecResult` (`stdout`, `stderr`, `exitCode`) |
|
||||||
|
|
||||||
|
`ExecError` carries the `ExecResult` so that timeout handlers still return partial output. This avoids losing buffered stdout/stderr when a long-running task is killed.
|
||||||
|
|
||||||
|
### Injection-Friendly Module Boundaries
|
||||||
|
|
||||||
|
Every non-trivial module accepts an `options` bag with injectable dependencies (`spawnSync`, `spawn`, `existsSync`, `whichSync`, `readFileSync`, etc.).
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- Unit tests can run without touching the real filesystem, `PATH`, or subprocess layer.
|
||||||
|
- The CLI itself injects its real dependencies through default parameters, so production behavior is unchanged.
|
||||||
|
- There is no global mocking required; each test provides its own narrow fakes.
|
||||||
|
|
||||||
|
### Minimal Dependency Surface
|
||||||
|
|
||||||
|
The runtime dependency graph contains exactly one external package: `minimist` (argument parsing). Everything else uses Node.js built-ins (`child_process`, `fs`, `os`, `path`).
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- Reduces supply-chain risk and install time.
|
||||||
|
- Avoids version-lock issues across Node.js 20+ environments.
|
||||||
|
- Keeps the compiled/bundled footprint negligible for a tool that is often installed as a sidecar.
|
||||||
|
|
||||||
|
## Extension Points
|
||||||
|
|
||||||
|
### Adding a New Client
|
||||||
|
|
||||||
|
To support a fourth (or fifth) AI CLI client, change four files in `src/` and the corresponding tests:
|
||||||
|
|
||||||
|
1. **`src/types.ts`** — Add the new name to the `ClientName` union type.
|
||||||
|
2. **`src/constants.ts`** — Append the new name to `CLIENT_NAMES`.
|
||||||
|
3. **`src/execute.ts`** — Add an entry to `CLIENT_ARGS` with the client’s native argument shape.
|
||||||
|
4. **`src/config.ts`** — No change required; the existing loop over `CLIENT_NAMES` automatically picks up the new env/flag/file keys.
|
||||||
|
5. **`src/dispatch.ts`** — Add a keyword check for the new client in `resolveClient`. Decide its precedence relative to existing keywords.
|
||||||
|
6. **Tests** — Add colocated test cases in `tests/dispatch.test.ts`, `tests/execute.test.ts`, and `tests/detect.test.ts`.
|
||||||
|
|
||||||
|
No changes are needed in `cli.ts` because it iterates over `CLIENT_NAMES` for validation.
|
||||||
|
|
||||||
|
### Streaming Support
|
||||||
|
|
||||||
|
If a future use case requires real-time output (e.g., long-running codegen with progressive feedback), the cleanest extension is to add an optional `onData` callback to `ExecuteOptions`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface ExecuteOptions {
|
||||||
|
clientPath?: string;
|
||||||
|
timeoutMs?: number;
|
||||||
|
spawn?: ...;
|
||||||
|
existsSync?: ...;
|
||||||
|
onData?: (chunk: string, stream: "stdout" | "stderr") => void;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When `onData` is provided, `executePrompt` would:
|
||||||
|
- Continue buffering internally for the final `ExecResult`.
|
||||||
|
- Also emit each chunk through `onData` so the caller can stream to a UI or logger.
|
||||||
|
- Reject/resolve with the same error taxonomy.
|
||||||
|
|
||||||
|
This preserves backward compatibility: existing callers that omit `onData` receive the exact same buffered `ExecResult` they get today.
|
||||||
|
|
||||||
|
### Platform Backends
|
||||||
|
|
||||||
|
The current Windows support is limited to discovery (`where` instead of `which`, `.exe` extension assumptions). If future clients require platform-specific spawn options (e.g., PowerShell quoting rules), the extension point is `CLIENT_ARGS` or a new `CLIENT_SPAWN_OPTIONS` record keyed by `ClientName`.
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
The test suite in `tests/` mirrors the `src/` structure:
|
||||||
|
|
||||||
|
| Test file | Coverage |
|
||||||
|
|---|---|
|
||||||
|
| `cli.test.ts` | Argument parsing, command routing, JSON/text output modes, exit codes, error formatting |
|
||||||
|
| `config.test.ts` | Layered precedence of flags, env, file, and `which` fallback; malformed JSON tolerance |
|
||||||
|
| `detect.test.ts` | `which` success/failure, PATH directory fallback, version parsing, missing binary handling |
|
||||||
|
| `dispatch.test.ts` | Keyword matching, case insensitivity, `--client` precedence, `defaultClient` fallback, invalid flag handling |
|
||||||
|
| `execute.test.ts` | Successful execution, stderr capture, non-zero exit codes, `ENOENT` → `ClientNotFoundError`, timeout, empty prompt rejection, special-character preservation |
|
||||||
|
|
||||||
|
All tests use injected mocks; no test spawns real client binaries or reads the real filesystem.
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
# Installation
|
||||||
|
|
||||||
|
This page covers installing the `ai-cli-dispatch` tool, its prerequisites, and post-install verification.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Node.js** ≥ 20 (required for `tsx`, `import` attributes, and modern `node:child_process` APIs)
|
||||||
|
- **npm** (bundled with Node.js)
|
||||||
|
- **Homebrew** (macOS/Linux) — recommended for installing the underlying AI CLI clients (`codex`, `claude`, `opencode`)
|
||||||
|
- One or more supported AI CLI clients:
|
||||||
|
- [Codex CLI](https://github.com/openai/codex)
|
||||||
|
- [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview)
|
||||||
|
- [OpenCode](https://github.com/opencode-ai/opencode)
|
||||||
|
|
||||||
|
## Install the tool
|
||||||
|
|
||||||
|
### 1. Clone the repository
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repository-url> ~/.openclaw/workspace/skills/ai-cli-dispatch
|
||||||
|
cd ~/.openclaw/workspace/skills/ai-cli-dispatch
|
||||||
|
```
|
||||||
|
|
||||||
|
If you already have the full `stef-openclaw-skills` repo checked out, use the path inside it instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.openclaw/workspace/skills/stef-openclaw-skills/tools/ai-cli-dispatch
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Install Node dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs:
|
||||||
|
- `tsx` — TypeScript execution runtime
|
||||||
|
- `minimist` — argument parsing
|
||||||
|
- `typescript` — type checking
|
||||||
|
|
||||||
|
## PATH configuration
|
||||||
|
|
||||||
|
The helper script lives at `scripts/ai-cli-dispatch`. Add it to your shell PATH so OpenClaw (or your terminal) can invoke it without a full path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ~/.zshrc or ~/.bashrc
|
||||||
|
export PATH="$HOME/.openclaw/workspace/skills/ai-cli-dispatch/scripts:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
Reload your shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source ~/.zshrc # or ~/.bashrc
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the script is reachable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
which ai-cli-dispatch
|
||||||
|
ai-cli-dispatch --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Optional configuration file
|
||||||
|
|
||||||
|
Create `~/.openclaw/ai-cli-dispatch.json` to customize client paths and set a default client:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.openclaw
|
||||||
|
$EDITOR ~/.openclaw/ai-cli-dispatch.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Example configuration:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"paths": {
|
||||||
|
"codex": "/opt/homebrew/bin/codex",
|
||||||
|
"claude": "/opt/homebrew/bin/claude",
|
||||||
|
"opencode": "/opt/homebrew/bin/opencode"
|
||||||
|
},
|
||||||
|
"defaultClient": "claude"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration precedence
|
||||||
|
|
||||||
|
When resolving a client binary, the tool checks sources in this order (first match wins):
|
||||||
|
|
||||||
|
1. CLI flag: `--codex-path`, `--claude-path`, `--opencode-path`
|
||||||
|
2. Environment variable: `AI_CLI_CODEX_PATH`, `AI_CLI_CLAUDE_PATH`, `AI_CLI_OPENCODE_PATH`
|
||||||
|
3. File config: `paths.<client>` in `~/.openclaw/ai-cli-dispatch.json`
|
||||||
|
4. System `PATH` lookup via `which` / `where`
|
||||||
|
|
||||||
|
The `defaultClient` follows the same precedence:
|
||||||
|
|
||||||
|
1. CLI flag: `--default-client`
|
||||||
|
2. Environment variable: `AI_CLI_DEFAULT_CLIENT`
|
||||||
|
3. File config: `defaultClient` in `~/.openclaw/ai-cli-dispatch.json`
|
||||||
|
|
||||||
|
## Install AI CLI clients
|
||||||
|
|
||||||
|
### Codex
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @openai/codex
|
||||||
|
```
|
||||||
|
|
||||||
|
### Claude Code
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @anthropic-ai/claude-code
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenCode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g @opencode-ai/opencode
|
||||||
|
```
|
||||||
|
|
||||||
|
Or via Homebrew where formulas are available:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install codex # if available in your tap
|
||||||
|
brew install claude-code # if available in your tap
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### 1. Check local tool health
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ai-cli-dispatch --help
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output:
|
||||||
|
|
||||||
|
```text
|
||||||
|
AI CLI Dispatch
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
ai-cli-dispatch list [--json|--text]
|
||||||
|
ai-cli-dispatch run --client <client> --prompt <prompt> [--json|--text]
|
||||||
|
ai-cli-dispatch dispatch <prompt> [--client <client>] [--json|--text]
|
||||||
|
ai-cli-dispatch --help
|
||||||
|
|
||||||
|
Clients: codex, claude, opencode
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. List discovered clients
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ai-cli-dispatch list --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Example output when two clients are installed:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "codex",
|
||||||
|
"path": "/opt/homebrew/bin/codex",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"found": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "claude",
|
||||||
|
"path": "/opt/homebrew/bin/claude",
|
||||||
|
"version": "0.7.8",
|
||||||
|
"found": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "opencode",
|
||||||
|
"found": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Run a quick dispatch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ai-cli-dispatch run --client codex --prompt "hello" --json
|
||||||
|
```
|
||||||
|
|
||||||
|
This should return a JSON result with `stdout`, `stderr`, and `exitCode`.
|
||||||
|
|
||||||
|
### 4. Test keyword dispatch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ai-cli-dispatch dispatch "refactor this using claude"
|
||||||
|
```
|
||||||
|
|
||||||
|
The tool inspects the prompt for client keywords (`claude`, `codex`, `opencode`, `open code`) and routes to the matching client.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### `Missing local Node dependencies for ai-cli-dispatch`
|
||||||
|
|
||||||
|
Run `npm install` from the skill directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ~/.openclaw/workspace/skills/ai-cli-dispatch
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Client "codex" not found or not installed`
|
||||||
|
|
||||||
|
- Ensure the client is installed globally or via Homebrew
|
||||||
|
- Verify it is on your PATH: `which codex`
|
||||||
|
- Or override the path in `~/.openclaw/ai-cli-dispatch.json` or with an environment variable
|
||||||
|
|
||||||
|
### `Prompt cannot be empty`
|
||||||
|
|
||||||
|
The `run` and `dispatch` commands require a non-empty `--prompt` or trailing prompt text.
|
||||||
|
|
||||||
|
### Config file is not being read
|
||||||
|
|
||||||
|
- Verify the file is at exactly `~/.openclaw/ai-cli-dispatch.json`
|
||||||
|
- Check for JSON syntax errors (trailing commas are not allowed)
|
||||||
|
- Use `--debug` for deeper troubleshooting if supported by the calling context
|
||||||
Reference in New Issue
Block a user