npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@elumixor/nitro-bot

v0.2.1

Published

Auto-generate an LLM chat-bot endpoint from your Nitro routes

Readme

@elumixor/nitro-bot

Auto-generate an LLM chat-bot endpoint from your existing Nitro routes.

How it works

  1. Install the package and enable the Nitro module.
  2. Mark any route that the LLM may call as a tool, by adding export const definition = tool({...}).
  3. Pass config (system prompt, adapters, etc.) directly to nitroBotModule({...}) in nitro.config.ts.

That's it. On dev/build the module scans your routes/ directory, finds the tool markers, and mounts a chat endpoint (default /chat) that lets an LLM call those routes via Nitro's in-process $fetch.

Setup

bun add @elumixor/nitro-bot
// nitro.config.ts
import { nitroBotModule } from "@elumixor/nitro-bot";

export default defineNitroConfig({
  modules: [
    nitroBotModule({
      endpoint: "/chat",
      source: "json",   // "query" | "json" | "form"
      field: "message", // name of the field carrying the user prompt
      systemPrompt: "You are a concise assistant. Use the provided tools when they cover what the user asks.",
    }),
  ],
});

Mark routes as tools

// src/routes/weather.get.ts
import { tool } from "@elumixor/nitro-bot";
import { defineEventHandler, getQuery } from "h3";
import { z } from "zod";

export const definition = tool({
  name: "get_weather",
  description: "Get the current weather for a city.",
  input: { city: z.string() },
});

export default defineEventHandler((event) => {
  const { city } = getQuery(event) as { city?: string };
  return { city, tempCelsius: 21 };
});

Reusing a schema from @elumixor/nitro-client

If your default export is a call whose first argument is an object literal with body and/or query keys (the shape used by @elumixor/nitro-client's handler()), the discovery scanner pulls that schema out at build time and uses it as the tool's input — you don't need to repeat it on tool():

// src/routes/greet.post.ts
import { tool } from "@elumixor/nitro-bot";
import { z } from "zod";
import { handler } from "../utils/handler"; // nitro-client's createHandler()

export const definition = tool({
  name: "greet",
  description: "Greet someone by name.",
});

export default handler(
  {
    body: {
      name: z.string().describe("The person to greet."),
      excited: z.boolean().optional().describe("Use an exclamation mark."),
    },
  },
  ({ body }) => ({ greeting: `Hello, ${body.name}${body.excited ? "!" : "."}` }),
);

How it works: nitro-bot parses each tool-marked route file with ts-morph. If the default export is handler(SCHEMA, fn) and SCHEMA.body / SCHEMA.query are object literals, the literal source is copied into the generated chat handler and used as the tool input — body and query are merged with body winning on collision. The z identifier (and any other identifier in the literal) must be importable from zod; non-zod helpers won't resolve. For those cases, pass input explicitly on tool() instead — explicit input always wins over auto-detection.

You get a /chat endpoint automatically:

curl -X POST localhost:3000/chat \
  -H 'content-type: application/json' \
  -d '{"message":"what is the weather in berlin"}'

Configuration

nitroBotModule({...}) accepts:

  • endpoint (default "/chat")
  • source — where the prompt comes from: "json" (default), "query", or "form"
  • field — the name of that field (default "message")
  • systemPrompt
  • model — any AI SDK LanguageModel (default "anthropic/claude-sonnet-4.6" via the Vercel AI Gateway)
  • maxSteps (default 8)

source also determines the HTTP method: "query"GET, "json" / "form"POST.

| source | Method | How to call | | --- | --- | --- | | "json" (default) | POST | curl -X POST localhost:3000/chat -H 'content-type: application/json' -d '{"message":"..."}' | | "query" | GET | curl -G 'http://localhost:3000/chat' --data-urlencode 'message=...' | | "form" | POST | curl -X POST localhost:3000/chat -F 'message=...' |

In each case, replace message with whatever you set field to.

Providers & env vars

model is the AI SDK's LanguageModel. It accepts two shapes:

1. Gateway string (default)

nitroBotModule({ model: "anthropic/claude-sonnet-4.6" });

Routed through the Vercel AI Gateway, which abstracts over Anthropic, OpenAI, Google, etc. and gives you per-model fallback + observability. Requires AI_GATEWAY_API_KEY in your environment (or the auto-injected VERCEL_OIDC_TOKEN when deployed on Vercel).

# .env (Nitro auto-loads this in dev)
AI_GATEWAY_API_KEY=sk-...

2. Direct provider

To skip the gateway, install a provider package and pass its env var:

| Provider package | Env var | | --- | --- | | @ai-sdk/anthropic | ANTHROPIC_API_KEY | | @ai-sdk/openai | OPENAI_API_KEY | | @ai-sdk/google | GOOGLE_GENERATIVE_AI_API_KEY | | @ai-sdk/mistral | MISTRAL_API_KEY | | @ai-sdk/groq | GROQ_API_KEY | | Gateway string | AI_GATEWAY_API_KEY |

Then pass model as a string in the AI SDK's provider/model form — nitroBotModule({...}) config is serialized into a generated runtime file at build time, so values must be plain data. For provider instances built via createAnthropic({ apiKey: ... }) and similar, construct them inside a route or your own runtime module instead of the module config.

Where to set the env var

  • Local dev: drop a .env next to your nitro.config.ts. Nitro auto-loads it.
  • Vercel: set it in Project Settings → Environment Variables, or run vercel env add.
  • Other hosts: set it like any other env var. The AI SDK reads process.env at request time, so no rebuild is needed when rotating keys.

maxSteps and tool-call limits

maxSteps (default 8) is the maximum number of model generation steps in one chat turn — not a tool-call counter. Internally we pass it as stopWhen: stepCountIs(maxSteps) to AI SDK generateText.

A "step" is one call to the model. The agent loop looks like this:

step 1: model decides → "call get_weather({city: 'Berlin'})"
        ↳ we run the tool, hand the result back
step 2: model decides → "call add({a: 17, b: 4})"
        ↳ we run the tool
step 3: model decides → final answer

So maxSteps: 8 allows roughly up to 7 sequential tool calls before the final answer is forced. Parallel tool calls within one step count as one step, so the practical limit can be higher.

The response includes the actual step count:

{ "text": "...", "steps": 2 }

If the model hits maxSteps, you'll still get whatever text it produced on the last step (it can't make another tool call). Pick a value that's comfortably above your typical depth — tight limits silently truncate complex queries.

Chat platforms (Telegram, Slack, Discord, ...)

Bring any Chat SDK adapter into your module config. Everything in adapters is started alongside the HTTP /chat endpoint and routed through the same agent.

bun add @chat-adapter/telegram   # or @chat-adapter/slack, @chat-adapter/discord, ...
// nitro.config.ts
import { nitroBotModule, telegramAdapter } from "@elumixor/nitro-bot";

export default defineNitroConfig({
  modules: [
    nitroBotModule({
      systemPrompt: "...",
      adapters: [
        telegramAdapter(),                                    // polling (reads TELEGRAM_BOT_TOKEN)
        // telegramAdapter({ webhookUrl: "https://app.example.com/telegram/webhook" }),
      ],
    }),
  ],
});

telegramAdapter() with no args runs in long-polling mode. Pass webhookUrl to switch to webhook mode — nitro-bot auto-mounts the receiving route at /telegram/webhook (override with webhookPath).

For other platforms (Slack, Discord, …), use the generic adapter() helper until a dedicated wrapper is added:

import { adapter } from "@elumixor/nitro-bot";

adapter({
  name: "slack",
  from: "@chat-adapter/slack",
  factory: "createSlackAdapter",
  options: { /* whatever createSlackAdapter expects */ },
  webhookPath: "/slack/events",
})

Why the indirection? Adapter instances need to be constructed at runtime, but nitro.config.ts is build-time code. The helpers return a serializable descriptor that nitro-bot emits into a generated runtime file.

The bot responds to DMs and @-mentions, and stays subscribed to the thread after the first reply. Available chat-adapter packages: telegram, slack, discord, teams, gchat, github, linear, whatsapp.

State defaults to in-memory (subscriptions reset on restart). Persistent state isn't yet wired through the inline config — drop into a runtime plugin if you need it before that lands.

What's next

Streaming, subagents, file attachments, and more chat-platform adapters (Slack / Discord / Teams) are on the roadmap.