Files
AgentHub/docs/superpowers/plans/2026-09-28-ai-tools-a1.md
T
nntrivi2001 77917203fd 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.
2026-09-29 08:18:40 +07:00

7.4 KiB

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 ()", 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.