From 77917203fdd3d862e72e8939079eb46abf4d4aa4 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Mon, 28 Sep 2026 10:57:10 +0700 Subject: [PATCH] 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 /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. --- bin/agenthub.js | 21 +- .../plans/2026-09-28-ai-tools-a1.md | 89 ++++++ server/__tests__/ai-tools-route.test.js | 135 +++++++++ server/__tests__/ai-tools.test.js | 215 ++++++++++++++ server/__tests__/lanes-api.test.js | 4 +- server/__tests__/lanes-lib.test.js | 2 +- server/__tests__/pty-run.test.js | 173 +++++++++-- server/__tests__/shell-path.test.js | 66 +++++ server/data/ai-tools/README.md | 15 + server/data/ai-tools/aider.json | 15 + server/data/ai-tools/amp.json | 16 + server/data/ai-tools/ante.json | 16 + server/data/ai-tools/antigravity.json | 19 ++ server/data/ai-tools/aug.json | 13 + server/data/ai-tools/autohand.json | 16 + server/data/ai-tools/claude.json | 9 + server/data/ai-tools/cline.json | 17 ++ server/data/ai-tools/codebuff.json | 13 + server/data/ai-tools/codex.json | 12 + server/data/ai-tools/command-code.json | 17 ++ server/data/ai-tools/continue.json | 17 ++ server/data/ai-tools/copilot.json | 15 + server/data/ai-tools/crush.json | 16 + server/data/ai-tools/cursor.json | 15 + server/data/ai-tools/devin.json | 19 ++ server/data/ai-tools/droid.json | 15 + server/data/ai-tools/gemini.json | 16 + server/data/ai-tools/goose.json | 16 + server/data/ai-tools/grok.json | 17 ++ server/data/ai-tools/hermes.json | 19 ++ server/data/ai-tools/kilo.json | 12 + server/data/ai-tools/kimi.json | 17 ++ server/data/ai-tools/kiro.json | 19 ++ server/data/ai-tools/mimo-code.json | 13 + server/data/ai-tools/mistral-vibe.json | 18 ++ server/data/ai-tools/muse.json | 19 ++ server/data/ai-tools/omp.json | 11 + server/data/ai-tools/openclaude.json | 14 + server/data/ai-tools/openclaw.json | 13 + server/data/ai-tools/opencode.json | 12 + server/data/ai-tools/opencode2.json | 15 + server/data/ai-tools/pi.json | 11 + server/data/ai-tools/prime-agent.json | 13 + server/data/ai-tools/qwen-code.json | 17 ++ server/data/ai-tools/rovo.json | 16 + server/data/ai-tools/trae.json | 16 + server/data/ai-tools/zcode.json | 17 ++ server/index.js | 9 + server/lib/ai-routers.js | 2 +- server/lib/ai-tools.js | 277 ++++++++++++++++++ server/lib/dashboard-runs.js | 2 +- server/lib/lanes.js | 17 +- server/lib/pty-run.js | 104 +++++-- server/lib/router-apply.js | 11 +- server/lib/shell-path.js | 51 ++++ server/lib/tmux.js | 14 + server/routes/ai-tools.js | 50 ++++ server/routes/lanes.js | 23 +- server/routes/run.js | 27 +- 59 files changed, 1804 insertions(+), 84 deletions(-) create mode 100644 docs/superpowers/plans/2026-09-28-ai-tools-a1.md create mode 100644 server/__tests__/ai-tools-route.test.js create mode 100644 server/__tests__/ai-tools.test.js create mode 100644 server/__tests__/shell-path.test.js create mode 100644 server/data/ai-tools/README.md create mode 100644 server/data/ai-tools/aider.json create mode 100644 server/data/ai-tools/amp.json create mode 100644 server/data/ai-tools/ante.json create mode 100644 server/data/ai-tools/antigravity.json create mode 100644 server/data/ai-tools/aug.json create mode 100644 server/data/ai-tools/autohand.json create mode 100644 server/data/ai-tools/claude.json create mode 100644 server/data/ai-tools/cline.json create mode 100644 server/data/ai-tools/codebuff.json create mode 100644 server/data/ai-tools/codex.json create mode 100644 server/data/ai-tools/command-code.json create mode 100644 server/data/ai-tools/continue.json create mode 100644 server/data/ai-tools/copilot.json create mode 100644 server/data/ai-tools/crush.json create mode 100644 server/data/ai-tools/cursor.json create mode 100644 server/data/ai-tools/devin.json create mode 100644 server/data/ai-tools/droid.json create mode 100644 server/data/ai-tools/gemini.json create mode 100644 server/data/ai-tools/goose.json create mode 100644 server/data/ai-tools/grok.json create mode 100644 server/data/ai-tools/hermes.json create mode 100644 server/data/ai-tools/kilo.json create mode 100644 server/data/ai-tools/kimi.json create mode 100644 server/data/ai-tools/kiro.json create mode 100644 server/data/ai-tools/mimo-code.json create mode 100644 server/data/ai-tools/mistral-vibe.json create mode 100644 server/data/ai-tools/muse.json create mode 100644 server/data/ai-tools/omp.json create mode 100644 server/data/ai-tools/openclaude.json create mode 100644 server/data/ai-tools/openclaw.json create mode 100644 server/data/ai-tools/opencode.json create mode 100644 server/data/ai-tools/opencode2.json create mode 100644 server/data/ai-tools/pi.json create mode 100644 server/data/ai-tools/prime-agent.json create mode 100644 server/data/ai-tools/qwen-code.json create mode 100644 server/data/ai-tools/rovo.json create mode 100644 server/data/ai-tools/trae.json create mode 100644 server/data/ai-tools/zcode.json create mode 100644 server/lib/ai-tools.js create mode 100644 server/lib/shell-path.js create mode 100644 server/routes/ai-tools.js diff --git a/bin/agenthub.js b/bin/agenthub.js index 676f1511..506ddd3f 100755 --- a/bin/agenthub.js +++ b/bin/agenthub.js @@ -1608,10 +1608,11 @@ async function cmdLanesAdd(args) { const pipeline = flag("pipeline"); const base = flag("base"); const slug = flag("slug"); + const tool = flag("tool"); if (repo) { if (cwd || pipeline) { console.error( - "usage: agenthub lanes add --repo [--title ] [--base ] [--slug ]" + "usage: agenthub lanes add --repo [--title ] [--base ] [--slug ] [--tool ]" ); process.exitCode = 1; return; @@ -1619,6 +1620,7 @@ async function cmdLanesAdd(args) { const body = { sourceRepo: repo, title: title || path.basename(repo) }; if (base) body.base = base; if (slug) body.slug = slug; + if (tool) body.tool = tool; const { lane: provisioning } = await post("/api/lanes/worktree", body); const deadline = Date.now() + 30_000; let lane = provisioning; @@ -1644,12 +1646,15 @@ async function cmdLanesAdd(args) { return; } if (!cwd || !title) { - console.error("usage: agenthub lanes add --cwd --title [--pipeline ]"); + console.error( + "usage: agenthub lanes add --cwd --title [--pipeline ] [--tool ]" + ); process.exitCode = 1; return; } const body = { cwd, title }; if (pipeline) body.pipeline = pipeline; + if (tool) body.tool = tool; const { lane } = await post("/api/lanes", body); console.log(`${c.green("✔")} Created lane #${lane.id}: ${lane.title}`); } @@ -2666,7 +2671,7 @@ async function cmdStage(args) { const stage = args[0]; if (!stage || stage.startsWith("--")) { console.error( - "usage: agenthub stage [--lane ] [--cwd ] [--tool claude|codex] [--status ] [--evidence ] [--note ] [--result pass|fail]" + "usage: agenthub stage [--lane ] [--cwd ] [--tool ] [--status ] [--evidence ] [--note ] [--result pass|fail]" ); process.exitCode = 1; return; @@ -2789,10 +2794,14 @@ const COMMAND_GROUPS = [ "Lanes", [ ["lanes", "", "List lanes with stage, liveness and progress"], - ["lanes add", "--cwd --title ", "Create a new lane bound to a directory"], [ "lanes add", - "--repo [--title ] [--base ] [--slug ]", + "--cwd --title [--tool ]", + "Create a new lane bound to a directory (--tool: an AI Tools id, default claude)", + ], + [ + "lanes add", + "--repo [--title ] [--base ] [--slug ] [--tool ]", "Provision a managed worktree lane", ], [ @@ -2871,7 +2880,7 @@ const COMMAND_GROUPS = [ [ "stage [flags]", "", - "Report the current pipeline stage for a lane (--tool claude|codex when one cwd has both)", + "Report the current pipeline stage for a lane (--tool when one cwd has lanes for two tools)", ], ["feature list", "[]", "List every feature this lane has activated, archived or live"], [ diff --git a/docs/superpowers/plans/2026-09-28-ai-tools-a1.md b/docs/superpowers/plans/2026-09-28-ai-tools-a1.md new file mode 100644 index 00000000..6ff9402d --- /dev/null +++ b/docs/superpowers/plans/2026-09-28-ai-tools-a1.md @@ -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: `/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ĩ `. +- 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` — 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 (