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:
@@ -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`.
|
||||
Reference in New Issue
Block a user