Files
AgentHub/docs/superpowers/plans/2026-09-28-ai-tools-a2.md
T

8.4 KiB
Raw Blame History

AI Tools A2 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: Install and update AI coding CLIs from Settings → AI Tools, keep the catalog fresh from the newest release, and show real icons.

Architecture: ai-tools.js learns two catalog fields and a third catalog tier (remote). Four small server modules own one job each: ai-tool-jobs.js (the one-at-a-time install/update runner), ai-tool-versions.js (npm-managed check + version badge), ai-tools-sync.js (daily remote catalog), ai-tool-icons.js (favicon cache). routes/ai-tools.js exposes them; the client polls one job endpoint.

Tech Stack: Node/Express (node:test), React + Vite + vitest.

Spec: docs/superpowers/specs/2026-09-28-ai-tools-a2-design.md

Global Constraints

  • Commands come only from catalog files; install/update/launcher are refused in DASHBOARD_AI_TOOLS_DIR files; allowed in built-in and remote (release) files.
  • package: ^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$; script url https, last two host labels equal the homepageUrl host's.
  • Every exec is an argv (execFile/spawn), never a shell string; stdin ignored; job timeout 10 min; log cap 256 KB; one job server-wide (409 EBUSY).
  • npm is resolved on the login shell's own PATH first (shellPath()), job env PATH = shellPath() + ":" + process.env.PATH.
  • npm Update only for npm-managed tools (binary realpath under npm prefix -g), else ENOTNPM.
  • Remote catalog only when release tag > running package.json version; AGENTHUB_AI_TOOLS_SYNC=0|false|off disables it; stored in <getDataDir()>/runtime/ai-tools-remote/.
  • Icons: no SVG; ≤ 64 KB; dotted hostname, no IP literal; headers nosniff, Content-Security-Policy: sandbox; stored in <getDataDir()>/runtime/ai-tool-icons/.
  • All writes behind sameOriginGuard.
  • Every new/edited 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. Never commit or push without the user's explicit ask.

Task 1: Catalog fields, trust tiers, shell PATH, repo URL

Files: Modify server/lib/ai-tools.js, server/lib/shell-path.js, server/lib/update-check.js, eight files in server/data/ai-tools/ (§ 1 table), server/data/ai-tools/README.md. Tests: server/__tests__/ai-tools.test.js, shell-path.test.js, update-check.test.js.

Produces:

  • Tool gains install: {kind:"npm",package}|{kind:"script",url}|null, update: {kind:"npm"}|{kind:"script"}|{kind:"self",args:string[]}|null; source ∈ builtin|remote|custom.

  • __setCatalogDirs([builtin, remote, custom]) (a two-element array keeps meaning [builtin, custom] for the A1 tests).

  • view() rows gain install, update; view().catalog from ai-tools-sync.catalogMeta() (Task 3; until then {source:"builtin"}).

  • shellPath(): string|null in shell-path.js — the last PATH the shell printed.

  • repoUrlFor(root): Promise<string|null> in update-check.js.

  • Failing tests: bad package name / http url / off-host url / update:npm without npm install / install in custom dir → file skipped; launcher allowed in remote tier; precedence custom › remote › builtin; shellPath() null before hydration, set after; repoUrlFor for a plugin-cache root uses the marketplace manifest.

  • Implement; add fields to codex, gemini, qwen-code, opencode, copilot, amp, cursor, claude.

  • npm run test:server green.

Task 2: Jobs and versions

Files: Create server/lib/ai-tool-jobs.js, server/lib/ai-tool-versions.js. Tests: server/__tests__/ai-tool-jobs.test.js, ai-tool-versions.test.js.

Produces:

  • ai-tool-versions.js: npmBin(): string|null (which("npm") over shellPath() then PATH); npmManaged(id): Promise<boolean>; versions({fresh}): Promise<{[id]:{npmManaged,installed,latest,updateAvailable}}> (24 h cache); invalidate(id); __setExec(fn) seam (fn(file,args,opts) → Promise<{stdout}>).

  • ai-tool-jobs.js: start(id, action) → job (throws ENOTOOL|ENOINSTALL|EINSTALLED|ENOUPDATE|ENOTINSTALLED|ENOTNPM|EBUSY|ENOTHING); startUpdateAll(); current(): Job|null; stepsFor(id, action): Promise<Step[]> where Step = {argv:string[]} | {script:url}; __setSpawn(fn), __setFetch(fn) seams.

  • Job end: hydrate, aiTools.refresh(), install ok ⇒ setEnabled(id,true), versions.invalidate(id).

  • Failing tests: argv for npm install/update is [npm,"install","-g","pkg@latest"] with npm from the shell PATH ahead of PATH; self is [detectedPath,...args]; script downloads (≤1 MB, https final URL, same two-label host) and runs ["bash", tmp] then deletes it; EBUSY while running; log capped at 256 KB keeping the tail; timeout ⇒ failed; EACCES ⇒ hint line; ok install ⇒ enabled; update-all steps = npm-managed with update + self updaters, ENOTHING when none; npm-managed by realpath under npm prefix -g; version parse from noisy --version; compare via update-check.compareVersions; offline ⇒ no badge.

  • Implement.

  • npm run test:server green.

Task 3: Remote catalog sync

Files: Create server/lib/ai-tools-sync.js; modify server/index.js (startBackgroundServices: first sync 60 s after start, then every 24 h, unref). Test: server/__tests__/ai-tools-sync.test.js.

Produces: syncOnce({root, fetch?}): Promise<{ok, reason?, tag?}>; remoteDir(): string; catalogMeta(): {source, tag, fetchedAt}; activeRemoteDir(): string|null (null when disabled, absent, or its tag is not newer than the running version) — used by ai-tools.load().

  • Failing tests (fake fetch): release not newer ⇒ nothing written; newer ⇒ files validated and written atomically with meta.json; an invalid file is dropped, not fatal; a failing download keeps the previous set; > 200 entries / > 64 KB file refused; AGENTHUB_AI_TOOLS_SYNC=0 ⇒ {ok:false, reason:"disabled"}; activeRemoteDir() null after the running version catches up.
  • Implement (contents URL = releaseApiUrl(repo) with /releases/latest replaced by /contents/server/data/ai-tools?ref=<tag>; each entry's download_url).
  • npm run test:server green.

Task 4: Icons

Files: Create server/lib/ai-tool-icons.js. Test: server/__tests__/ai-tool-icons.test.js.

Produces: iconFor(id, {fetch?}): Promise<{type, body}|null> (cache hit, 24 h miss cache); iconDomain(tool): string|null.

  • Failing tests: svg refused; > 64 KB refused; IP-literal / port / dotless domain ⇒ null; redirect off-domain refused; cache hit makes no second fetch; miss cached.
  • Implement.

Task 5: Routes

Files: Modify server/routes/ai-tools.js, server/openapi-extra/ai-tools.js, regenerate server/openapi.yaml (npm run openapi if present, else the existing generator script). Test: server/__tests__/ai-tools-route.test.js.

  • Failing tests: POST /:id/install 202 + job, 400 ENOINSTALL, 409 EBUSY, cross-origin 403; POST /:id/update 409 ENOTNPM; POST /update-all 400 ENOTHING; GET /job null then job; GET /versions shape; GET /:id/icon headers and 404.
  • Implement (error code → status map in one helper).

Task 6: Client

Files: Modify client/src/lib/api.ts, client/src/hooks/useAiTools.ts, client/src/components/settings/AiToolsSettings.tsx, client/src/i18n/locales/{en,vi}/settings.json (wherever aiTools.* keys live). Test: client/src/components/settings/__tests__/AiToolsSettings.test.tsx (create or extend).

  • Failing tests: Install opens a confirm dialog with npm install -g <pkg>@latest; confirming POSTs and renders the polled log; polling stops at ok; badge v<latest> available; Installed outside npm when not npm-managed; Update tools disabled with nothing to do; <img> falls back to the letter on error.
  • Implement; review any screen snapshot diff before -u.
  • cd client && NODE_ENV=test npx vitest run green; npx tsc --noEmit clean.

Task 7: Docs + browser check

  • CLAUDE.md marker, AGENTS.md, docs/API.md, server/README.md, server/data/ai-tools/README.md, env var list (AGENTHUB_AI_TOOLS_SYNC); bash skills/file-headers/scripts/check-headers.sh exits 0.
  • Browser check on an isolated server (DASHBOARD_DATA_DIR, TMUX_TMPDIR temp, port 4850): icons render, Install dialog text, badge. No real install is run without the user's go.