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 arriveWhere things live
| Path | Purpose |
|---|---|
config/tools.config.ts | The 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.ts | The only file that reads OPENROUTER_API_KEY; converts OpenRouter’s SSE stream into plain text |
app/api/ai/[slug]/route.ts | The single Route Handler that serves all ten tools |
components/tools/tool-shell.tsx | Renders any tool’s form and output from its registry entry |
lib/store/settings-store.ts | Zustand store for model, temperature, and max tokens |
services/tool.service.ts | Framework-agnostic helpers (like default input values) shared by the web form and the CLI |
src/cli/index.ts | The CLI entry point — generates one subcommand per tool from the same registry |
scripts/build-cli.mjs | esbuild 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.tsdirectly, so it stays in sync with the web app by construction rather than by duplicating logic.