# 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 `/runtime/ai-tools-remote/`. - Icons: no SVG; ≤ 64 KB; dotted hostname, no IP literal; headers `nosniff`, `Content-Security-Policy: sandbox`; stored in `/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ĩ `. - 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` 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`; `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` 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=`; 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 @latest`; confirming POSTs and renders the polled log; polling stops at `ok`; badge `v available`; `Installed outside npm` when not npm-managed; **Update tools** disabled with nothing to do; `` 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.