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.
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/requiresentries are bare names, no/. launcheris 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 withAGENTHUB_SHELL_PATH=0. routerCapable=claude,codexonly.- 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)(throwsENOTOOL/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;launcherin custom file skipped; custom id replaces built-in; detection over a temp PATH with an executable and a non-executable file;requiresmissing ⇒ not installed; enable/default rules (default falls back to claude, then first installed enabled; disabling the default clears it;EBADDEFAULTfor not-installed/disabled;ENOTOOLunknown). -
Implement; write the 38 JSON files from the Orca table (spec §1 mapping); README attribution (Orca MIT).
-
npm run test:servergreen.
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 behindsameOriginGuard.- Route test (pattern:
git-credentials-route.test.js): GET shape, PUT enable/disable, default errors,/api/run/binary?tool=nope400,?tool=antigravityresolves 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 ⇒ 400EDISABLED.buildCatalogArgv(tool, {resumeLast, bypass, initialPrompt}, bin)→[bin, ...args, ...resume?, ...bypass?, ...prompt];after-start⇒setTimeout(delayMs)thentmux.pasteText(id, prompt)if the session still exists.- Every spawn env carries
PATH: process.env.PATH; catalog tools start bydetect().path(fallback firstdetectname);bypassEnvmerged whenbypass. - Codex:
bypass⇒ catalog codex bypass args, and-s/-adropped. - Unknown tool ⇒
ENOTOOLat spawn. - router-apply: non-
routerCapableaccepts 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 callsupdate(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.