DevTools AI

Architecture

Every tool — regardless of what it does — flows through the same path, so there’s one place to fix bugs and one place to add features.

Request flow

The web app and the CLI both end up calling the same services/ai-service.ts — the web app through an HTTP round trip, the CLI by importing it directly, since it runs in the same Node process.

Web:  ToolShell (client)
        -> useAiCompletion hook
        -> POST /api/ai/[slug]
             -> tools.config.ts  (look up tool by slug)
             -> tool.schema.safeParse(input)  (Zod validation)
             -> services/ai-service.ts  (build prompt + call OpenRouter)
             -> lib/ai/openrouter-client.ts  (server-only, streams response)
        <- plain-text stream
        -> ToolOutputPanel renders markdown / code / text

CLI:  src/cli/index.ts  (one Commander subcommand per registry entry)
        -> runToolCommand()  (collect --flags, file/stdin, or --example)
        -> tool.schema.safeParse(input)  (same Zod validation)
        -> services/ai-service.ts  (same call, no HTTP hop)
        -> process.stdout.write() as chunks arrive

Where things live

PathPurpose
config/tools.config.tsThe tool registry — fields, example input, output format, schema, and prompt builder for all ten tools
lib/ai/prompts/One file per tool: its Zod schema and prompt builder
lib/ai/openrouter-client.tsThe only file that reads OPENROUTER_API_KEY; converts OpenRouter’s SSE stream into plain text
app/api/ai/[slug]/route.tsThe single Route Handler that serves all ten tools
components/tools/tool-shell.tsxRenders any tool’s form and output from its registry entry
lib/store/settings-store.tsZustand store for model, temperature, and max tokens
services/tool.service.tsFramework-agnostic helpers (like default input values) shared by the web form and the CLI
src/cli/index.tsThe CLI entry point — generates one subcommand per tool from the same registry
scripts/build-cli.mjsesbuild bundles src/cli into dist/cli/index.mjs, resolving every @/ import to a real relative path — the published npm package is this one file, no TypeScript loader needed at runtime

Design choices

  • One dynamic route, not ten. Adding a tool means adding a registry entry, not a new API route or page.
  • Model allowlisting. The client can request any model in config/models.ts, but never an arbitrary string — the route rejects anything else.
  • No server-side storage. There is no database. Nothing about a request is persisted after the response streams back.
  • One registry, two frontends. The CLI never calls the HTTP route — it imports services/ai-service.ts directly, so it stays in sync with the web app by construction rather than by duplicating logic.