feat(ai-tools): catalog of AI coding CLIs, detection, enable/default, lanes for any tool (#40 A1)

server/lib/ai-tools.js loads server/data/ai-tools/*.json (38 tools, data from
Orca, MIT) plus DASHBOARD_AI_TOOLS_DIR, detects each CLI by walking PATH, and
keeps enable/default in <data dir>/ai-tools.json. shell-path.js merges the
login shell's PATH at startup and on Refresh. /api/ai-tools lists, refreshes
and toggles; /api/run/binary resolves through the catalog.

Lanes accept any catalog id (EDISABLED for a disabled one on creation, ENOTOOL
at start once a tool left the catalog). pty-run keeps the Claude/Codex
builders and builds every other tool's argv from its catalog file, pasting an
after-start prompt through a tmux buffer; every pane gets the hydrated PATH.
A per-run bypass switch appends the catalog's bypass args (Codex included).
POST /api/run now forwards sandbox/approval/resumeLast, which it dropped.
agenthub lanes add gains --tool.
This commit is contained in:
2026-09-28 10:57:10 +07:00
parent 0aa7fc232e
commit 77917203fd
59 changed files with 1804 additions and 84 deletions
@@ -0,0 +1,89 @@
# AI Tools A1 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** AgentHub knows ~38 AI coding CLIs from a JSON catalog, detects which are installed, lets the user enable/disable them and pick a default, and runs a lane on any of them.
**Architecture:** One server module `server/lib/ai-tools.js` owns catalog load/validation, PATH detection and the enable/default state file; `server/lib/shell-path.js` hydrates `process.env.PATH` from the login shell. `pty-run.js` keeps its Claude/Codex builders (`launcher`) and gains one generic catalog builder. The client reads everything through one hook, `useAiTools`.
**Tech Stack:** Node/Express + better-sqlite3 (server, `node:test`), React + Vite + vitest (client), tmux.
**Spec:** `docs/superpowers/specs/2026-09-28-ai-tools-a1-design.md`
## Global Constraints
- Catalog id: `^[a-z0-9][a-z0-9-]{0,39}$`; `detect`/`requires` entries are bare names, no `/`.
- `launcher` is accepted only in built-in files (`"claude"|"codex"`).
- Args and env only ever come from the catalog; the prompt is ONE argv element or a tmux paste buffer; never a shell string.
- Bypass args are emitted only when the run request carries `bypass: true`; never remembered.
- State file: `<getDataDir()>/ai-tools.json` = `{ "disabled": [], "default": null }`.
- PATH hydration: `$SHELL` (fallback `/bin/bash`) `-ilc`, delimiters, 5 s timeout, stdin ignored, after listen, off with `AGENTHUB_SHELL_PATH=0`.
- `routerCapable` = `claude`, `codex` only.
- Every new source file starts with the header ending `@author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>`.
- Server tests: `npm run test:server`; client: `cd client && NODE_ENV=test npx vitest run`; commits: `NODE_ENV=test git commit` (hook runs both suites, long timeout).
- Never push without an explicit ask.
---
### Task 1: Catalog, detection, state (`server/lib/ai-tools.js`)
**Files:**
- Create: `server/lib/ai-tools.js`, `server/data/ai-tools/*.json` (38 files), `server/data/ai-tools/README.md`
- Test: `server/__tests__/ai-tools.test.js`
**Interfaces (Produces):**
- `list(): Tool[]` — catalog order: `claude`, `codex`, then by label.
- `get(id): Tool|null`, `has(id): boolean`
- `detect(id): {installed, path, command}` (cached), `refresh()` clears the cache and re-reads the catalog.
- `readState(): {disabled: string[], default: string|null}`, `isEnabled(id)`, `setEnabled(id, bool)`, `setDefault(id)` (throws `ENOTOOL` / `EBADDEFAULT`), `effectiveDefault(): string|null`
- `view(): {tools: ToolView[], default}` — the GET body.
- `ROUTER_CAPABLE = new Set(["claude","codex"])`
- `__setCatalogDirs([...])` test seam (built-in dir, custom dir).
- `Tool = {id,label,detect,requires,args,homepageUrl,faviconDomain,prompt,resume,bypass,bypassEnv,launcher,source}`.
- [ ] Write failing tests: bad id skipped; `/` in detect skipped; non-array args skipped; `launcher` in custom file skipped; custom id replaces built-in; detection over a temp PATH with an executable and a non-executable file; `requires` missing ⇒ not installed; enable/default rules (default falls back to claude, then first installed enabled; disabling the default clears it; `EBADDEFAULT` for not-installed/disabled; `ENOTOOL` unknown).
- [ ] Implement; write the 38 JSON files from the Orca table (spec §1 mapping); README attribution (Orca MIT).
- [ ] `npm run test:server` green.
### Task 2: PATH hydration (`server/lib/shell-path.js`)
**Interfaces:** `hydrateShellPath({spawn?, shell?, timeoutMs?}) : Promise<boolean>` — merges new segments into `process.env.PATH`, returns whether it ran.
- [ ] Tests with an injected `spawn` (execFile-like): delimiters with noisy rc output, timeout, spawn error, `AGENTHUB_SHELL_PATH=0`, no duplicate segments.
- [ ] Implement with `child_process.execFile(shell, ["-ilc", "printf '__AGENTHUB_PATH__%s__AGENTHUB_PATH__' \"$PATH\""], {timeout:5000, stdio ignore stdin})`.
### Task 3: Routes `/api/ai-tools`
**Files:** Create `server/routes/ai-tools.js`, `server/openapi-extra/ai-tools.js`; modify `server/index.js` (mount + hydrate after listen), `server/openapi-extra.js`, `server/routes/run.js` (binary probe via catalog).
- `GET /` → `view()`; `POST /refresh` → hydrate, `refresh()`, `view()`; `PUT /:id {enabled?, default?:true}`. Mutations behind `sameOriginGuard`.
- [ ] Route test (pattern: `git-credentials-route.test.js`): GET shape, PUT enable/disable, default errors, `/api/run/binary?tool=nope` 400, `?tool=antigravity` resolves through catalog.
### Task 4: Lanes and spawn on any tool
**Files:** `server/lib/lanes.js`, `server/routes/lanes.js`, `server/lib/pty-run.js`, `server/lib/tmux.js` (`pasteText`), `server/lib/dashboard-runs.js`, `server/lib/router-apply.js`, `server/lib/ai-routers.js` (builtin label "Native"), `server/routes/run.js` (forward sandbox/approval/resumeLast/bypass), `bin/agenthub.js` (`lanes add --tool`).
- `VALID_TOOLS` ⇒ `aiTools.has`; lane creation on a disabled tool ⇒ 400 `EDISABLED`.
- `buildCatalogArgv(tool, {resumeLast, bypass, initialPrompt}, bin)` → `[bin, ...args, ...resume?, ...bypass?, ...prompt]`; `after-start` ⇒ `setTimeout(delayMs)` then `tmux.pasteText(id, prompt)` if the session still exists.
- Every spawn env carries `PATH: process.env.PATH`; catalog tools start by `detect().path` (fallback first `detect` name); `bypassEnv` merged when `bypass`.
- Codex: `bypass` ⇒ catalog codex bypass args, and `-s`/`-a` dropped.
- Unknown tool ⇒ `ENOTOOL` at spawn.
- router-apply: non-`routerCapable` accepts only native/builtin.
- [ ] Tests: argv per prompt mode, resume, bypass on/off, Codex bypass, Claude ignores bypass, after-start paste scheduling (fake timers), PATH in env, EDISABLED/EBADTOOL/not-installed allowed, router-apply generic, dashboard-runs any id.
### Task 5: Client data layer
**Files:** `client/src/lib/api.ts` (`aiTools.list/refresh/update`, `AiTool` type, `RunStartArgs.bypass`), `client/src/lib/types.ts` (`LaneTool = string`, drop `LANE_TOOL_LABEL`), create `client/src/hooks/useAiTools.ts` (`useAiTools(): {tools, default, byId, label(id), reload, loading}` — module-level shared cache + subscribers, refetch on focus), replace all `LANE_TOOL_LABEL` uses and the Workspace per-tool binary probes.
### Task 6: Settings → AI Tools section
**Files:** Create `client/src/components/settings/AiToolsSettings.tsx`; modify `client/src/pages/Settings.tsx` (TOC + section), i18n `settings.json` en/vi; hide Claude/Codex tabs while disabled (settings hub nav).
- [ ] Test `client/src/components/settings/__tests__/AiToolsSettings.test.tsx`: rows, toggle calls `update(id,{enabled})`, set default, refresh, available rows show only link.
### Task 7: Add lane + router row + run form
**Files:** `AddLaneModal.tsx` (Default / Other… select, empty state link), `LaneCard.tsx` `LaneRouterRow` ("Native (<label>)", router options disabled for non-routerCapable), `Workspace.tsx`, `RunSetup.tsx` (catalog form: resume, red bypass checkbox; Codex bypass), `LaneConsolePane.tsx`, i18n.
- [ ] Tests updated/added for Default/Other, disabled router option, resume + bypass on the catalog form, Codex bypass.
### Task 8: Docs + full verification
- CLAUDE.md (tool rule, bypass rule, router label, marker), AGENTS.md, docs/LANES.md, docs/API.md, docs/CLI.md, server/README.md, `npm run openapi:yaml`.
- `npm run test:server`, `cd client && NODE_ENV=test npx vitest run` (review snapshot diffs), `npx tsc --noEmit`, `bash skills/file-headers/scripts/check-headers.sh`.