# AI Tools — part A1: catalog, detection, enable/default, lanes for any tool Issue: #40. Date: 2026-09-28. Status: design agreed in chat, awaiting spec review. Part of a four-part split. **A1** (this spec) makes AgentHub know about many AI coding CLIs and run a lane on any of them. **A2** adds Install / Update, remote catalog sync and favicons (decisions recorded at the end, own spec later). **B** moves per-tool settings into each tool's Settings tab. **C** brings Codex to parity (token usage + pricing, hooks). **D** does the same for Antigravity after a spike. References studied: Orca (`stablyai/orca`, MIT) and 9Router (npm `9router` 0.5.91, MIT). Catalog data (commands, flags, homepages) is taken from Orca's `agent-catalog.tsx`, `tui-agent-config.ts` and `tui-agent-permissions.ts`, with attribution in `server/data/ai-tools/README.md`. ## Goals - Settings → Dashboard gets an **AI Tools** section: **Installed (N detected)** with **Refresh**, and **Available to install**. Each row: icon (letter avatar in A1, favicon in A2), label, command, **Enabled / Disabled**, **Set default / ✓ Default**, homepage link ↗. - A newly installed CLI shows up without editing any config: the catalog is broad (~40 tools, Oh My Pi included), detection re-reads the user's login-shell PATH, and (A2) the catalog updates itself. - A lane can be created for any installed, enabled tool; it runs the tool's CLI in the lane's tmux pane. - A disabled tool is hidden from Add lane and its Settings tab, and the server refuses a NEW lane for it. Existing lanes keep working. ## Non-goals (A1) Installing or updating a tool, remote catalog sync, favicons (A2). Per-tool settings tabs, pricing and hooks for non-Claude tools (B, C, D). Per-tool command/args/env overrides in the UI (Orca has them; rejected — they let a browser choose an executable for the server to run; a custom JSON file in `DASHBOARD_AI_TOOLS_DIR` covers the need). Heuristic discovery of CLIs that no catalog lists. ## 1. Catalog One JSON file per tool in `server/data/ai-tools/.json`, plus any `*.json` in `DASHBOARD_AI_TOOLS_DIR` (same override rule as `DASHBOARD_PIPELINES_DIR`: a custom file with a built-in id replaces it). ```json { "id": "antigravity", "label": "Antigravity", "detect": ["agy"], "homepageUrl": "https://antigravity.google/docs/cli/overview", "faviconDomain": "antigravity.google", "prompt": { "mode": "flag", "flag": "-i" }, "resume": ["--continue"], "bypass": ["--dangerously-skip-permissions"] } ``` - `id` — `^[a-z0-9][a-z0-9-]{0,39}$`. It is what `lanes.tool` stores. - `detect` — bare command names, no `/`. The first one found on PATH is also the command the lane runs (Orca's `detectCmd` + aliases). - `requires` (optional) — other commands that must also be on PATH. - `args` (optional) — fixed args right after the command, before anything else (e.g. Hermes needs `["chat"]` before its `--query` prompt flag). - `prompt` — how the first prompt reaches the tool: - `{ "mode": "argv", "separator": "--" }` — trailing positional, optional option terminator before it; - `{ "mode": "flag", "flag": "-i" }` — `[flag, prompt]`; - `{ "mode": "after-start", "delayMs": 3000 }` — start the bare CLI, then paste the prompt into the pane after `delayMs` (`ponytail:` fixed delay; add readiness detection like Orca's if a tool loses prompts). - `resume` (optional) — args that continue the last session. Absent: no Resume button. - `bypass` / `bypassEnv` (optional) — args / env that turn off the tool's permission prompts. Absent: no bypass switch. - `launcher` (built-ins only) — `"claude"` or `"codex"`: the lane keeps using the existing hand-written argv builders (model, effort, permission mode, sandbox, approval, AI Router). A custom file may not set it. - `faviconDomain`, `install` — used by A2; ignored in A1. **Where the data comes from.** Orca ships and exercises this data for ~40 tools, so it is the source: `detectCmd`+aliases → `detect`, `requires` from `detectRequiredCommands`, prompt modes mapped as `argv` → `argv` (+ its `argvPromptSeparator`), `flag-prompt` → flag `--prompt`, `flag-prompt-interactive` → flag `--prompt-interactive`, `flag-interactive` → flag `-i`, `stdin-after-start` → `after-start`, `hermes-query` → `args ["chat"]` + flag `--query`; `YOLO_TUI_AGENT_ARGS` split into argv arrays → `bypass`, `YOLO_TUI_AGENT_ENV` → `bypassEnv`. `resume` is filled only where the tool's `--help` or official docs confirm a continue-last flag; anything unconfirmed is **omitted**, not guessed. Tools whose launch is not "their own command" (Orca's `claude-agent-teams`, which launches through Orca itself) are left out. **Validation** (`server/lib/ai-tools.js`): a file that fails any rule above is skipped with one log line; it never throws at startup. Duplicate ids: custom beats built-in, else first file wins. Antigravity moves from hard-coded `pty-run.js` code to its catalog file. ## 2. Detection - `detectTools()` resolves each tool's `detect` names by walking the PATH directories with `fs` and checking for an executable file — no process per tool (Windows is out of scope; AgentHub runs on Linux/macOS/WSL). Cached until the next refresh. - **PATH hydration.** The server's PATH is whatever the systemd unit / LaunchAgent captured at install time, so a CLI installed later into a new directory is invisible. At startup and on Refresh, run the user's `$SHELL` (fallback `/bin/bash`) with `-ilc` and a command that prints `$PATH` between two delimiters, with a 5 s timeout; the text between the delimiters is merged into `process.env.PATH` (new segments appended). Interactive (`-i`) because nvm and most installers add to PATH in `.bashrc`, which `-lc` skips; the delimiters discard whatever the rc files print. stdin is `ignore`, so an rc file that waits for input or auto-attaches tmux gets EOF instead of hanging until the timeout. Failure keeps the existing PATH. It runs after the server is listening, never before. Off with `AGENTHUB_SHELL_PATH=0` for users whose rc files must not run from a service. Taken from Orca's `hydrate-shell-path.ts`. - Result per tool: `installed`, `path` (resolved binary), `command` (the name that matched). - **The pane gets the same PATH.** A lane pane's environment comes from the tmux server, not from AgentHub's process, and an npm-installed CLI is a `#!/usr/bin/env node` script that dies without `node` on PATH. So every spawn passes the hydrated `PATH` through `tmux new-session -e` / `respawn-pane -e` (the channel `AGENTHUB_LANE_ID` already uses), and a catalog tool is started by its resolved absolute `path`. ## 3. Enable / default Stored in `/ai-tools.json`: `{ "disabled": ["grok"], "default": "claude" }`. - Tools are enabled unless listed in `disabled` — a freshly detected or installed tool is on. - `default`: null or an id. Effective default = `default` if it is installed and enabled, else `claude` if installed and enabled, else the first installed enabled tool in catalog order, else null. - Only ids present in the catalog are accepted. Disabling the default clears it. ## 4. API All mutating routes sit behind the existing `sameOriginGuard`. - `GET /api/ai-tools` → `{ tools: [{ id, label, command, homepageUrl, installed, path, enabled, source: "builtin"|"custom", launcher, prompt: {mode}, resume: bool, bypass: bool, routerCapable: bool }], default: id|null }`. `routerCapable` is true for `claude` and `codex` only. Arg arrays are not returned. - `POST /api/ai-tools/refresh` → hydrate PATH, re-detect, same body as GET. - `PUT /api/ai-tools/:id` with `{ enabled?: boolean, default?: true }` → same body as GET. Unknown id 404 `ENOTOOL`; making a not-installed or disabled tool the default 400 `EBADDEFAULT`. - `GET /api/run/binary?tool=` keeps its shape (`{found, path}`) but resolves through the catalog; an unknown id stays 400 `EBADTOOL`. ## 5. Lanes on any tool - `lanes.tool` accepts any catalog id. `VALID_TOOLS` (in `lanes.js`, `pty-run.js`, `routes/lanes.js`) is replaced by `aiTools.has(id)`. No schema change: `lanes.tool` and `dashboard_runs.tool` are unconstrained TEXT, and `idx_lanes_cwd_tool` still allows one lane per (cwd, tool). - Creating a lane (`POST /api/lanes`, `/ensure`, `/worktree`) for a disabled tool → 400 `EDISABLED`; unknown → 400 `EBADTOOL`. A tool that is not installed may still get a lane (it can be installed afterwards); Start then fails with the existing missing-binary banner. - `agenthub lanes add` has no `--tool` flag today; it gains `--tool ` (default `claude`), and the help strings saying `--tool claude|codex` become `--tool `. - **Spawn** (`pty-run.js`): `launcher: "claude"|"codex"` → the existing builders. Any other tool → `[path, ...args, ...resume?, ...bypass?, ...prompt]` per the prompt mode; `after-start` sets a named tmux buffer with `set-buffer -b -- ` and pastes it with `paste-buffer -p -d -b ` + Enter after `delayMs` (argv only — the tmux wrapper has no stdin), skipped if the pane is gone. `POST /api/run` and `POST /api/lanes/:id/start` both forward `bypass`. Args come only from the catalog; the prompt is always ONE argv element or a paste buffer, never part of a shell string. - **Bypass switch.** The run request carries `bypass: true`; the server appends the catalog's `bypass` args / `bypassEnv`. Codex gains it (`--dangerously-bypass-approvals-and-sandbox`); Claude does not (its `bypassPermissions` permission mode already exists). Default off, never remembered between runs. This replaces CLAUDE.md's "bypass flags are never emitted" rule with "only when the user ticks the switch for that run". - A lane whose tool left the catalog: label falls back to the id, Start → 400 `ENOTOOL`, the lane is not touched otherwise. - Non-Claude tools: no pipeline, no agents install (as codex/antigravity today); running/idle comes from the exited-pane heal sweep. - Places that list tools by name today and must follow the catalog instead: `dashboard-runs.js` (stores any id, not codex/antigravity-or-claude), `router-apply.js` (the Antigravity special case becomes "a tool that is not `routerCapable` accepts only `native`, else `EUNSUPPORTED`"), `routes/run.js` (binary probe). Filters that mean exactly one tool stay (`codex-ingest.js`, `cc-reload.js`, hook ingestion pinned to `claude`). ## 6. UI - `client/src/hooks/useAiTools.ts` — fetches `/api/ai-tools`, refetches on window focus and after a mutation. Replaces `LANE_TOOL_LABEL` and the Workspace's per-tool binary probes; `LaneTool` becomes `string`. - `client/src/components/settings/AiToolsSettings.tsx` — the AI Tools section of Settings → Dashboard (Settings.tsx is ~1,900 lines already), with its own table-of-contents entry. Rows as in Goals; A1 shows a letter avatar and no Install button. An "Available to install" row has only the homepage link — no Enabled/Default controls for a tool that is not there. - **Add lane — Which CLI:** two buttons, **Default (