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

@davepi/agent

v0.6.1

Published

Chat agent that ships pre-wired against a dAvePi backend. HTTP /chat + Slack + Telegram channels, OpenAI / Anthropic / OpenRouter / Ollama (local) providers, service-account or per-user OAuth-link auth. Consumes the dAvePi MCP server, so ACL/scope/tenant

Readme

@davepi/agent

A chat agent that ships pre-wired against a dAvePi backend. The agent connects to davepi's built-in MCP server, so every schema's CRUD + relations + aggregations + audit + file ops are available as tools out of the box — and tenant isolation and ACL are enforced server-side, not in the prompt.

What you get

  • HTTP /chat endpoint with Server-Sent Events streaming.
  • Slack bot via @slack/bolt (app mention + DM). Render tools translate to Block Kit tables and QuickChart images.
  • Telegram bot via the Bot API over long-polling — no public URL needed, just a TELEGRAM_BOT_TOKEN (DM + group @-mention). Render tools translate to monospace tables and QuickChart photos.
  • WhatsApp / Embeddable widget templates in lib/channels/templates/ — stubs with a one-screen recipe for filling in.
  • OpenAI, Anthropic, OpenRouter, and Ollama (local) providers via the Vercel AI SDK. Switch via LLM_PROVIDER. OpenRouter fronts hundreds of models behind one key — see Models via OpenRouter below. Ollama runs against a local model server with no API key — see Local models via Ollama.
  • Two auth modes:
    • service — one JWT (or X-Client-Id) for the whole bot. Right for an anonymous storefront widget where every visitor sees the same role-scoped slice.
    • per-user — each channel user maps to a real davepi user via an OAuth-style link flow. Refresh tokens stored locally; access tokens minted on demand and cached.
  • Tool router for backends with too many schemas: above the configured limit (default 40), the agent first picks a resource, then loads that resource's tools.
  • Structured render tools (render_table, render_chart) so the model can ask for a visualization without each channel reinventing layout.
  • Team delegation (optional): give an agent a team roster and it becomes a leader that can delegate subtasks to specialist agents in parallel, assess the results, and iterate. Specialists share the leader's identity and differ only by persona / skills / memory — capability, never privilege. See the Teams and delegation guide.

ACL boundary — design rule

The JWT (or X-Client-Id) is the access boundary. The agent never re-implements ACL checks and never tries to constrain results via prompt text. If you want a service-account bot to only see "published" rows, declare a schema.acl.scope[role] filter on the davepi side — the MCP server applies it on every read and the agent never sees the filter itself. The wrong pattern (and the one to avoid) is a broad service token plus "only show user X's data" in the prompt; that's a confused-deputy bug waiting to happen.

Quick start

# In your davepi project, install the agent as a dev tool:
npm install @davepi/agent

# Minimal env:
export DAVEPI_URL=http://localhost:5050
export ANTHROPIC_API_KEY=sk-ant-...
export DAVEPI_BEARER=dpk_...   # a davepi API key (recommended: long-lived, revocable)
# OR for anonymous reads via an apiClient role:
# export DAVEPI_CLIENT_ID=<client-id>

# Start the agent:
npx davepi-agent
# HTTP /chat is now listening on :5070

# Talk to it:
curl -N -X POST http://localhost:5070/chat \
  -H 'content-type: application/json' \
  -d '{"message":"What products do we have?"}'

HTTP endpoints

| Method + path | Purpose | | ------------------------- | ------------------------------------------------------------------------------------------- | | GET /health | Liveness + agent identity (agentKey, name, isTeam, team[]) for the admin portal. | | POST /chat | SSE-streaming chat. Body: { message, history?, stream?, conversationId? }. | | GET /conversations | List past conversations (metadata + preview, newest first) for a resume UI. | | GET /conversations/:id | One conversation's full transcript, shaped as POST /chat's history so you can resume it. | | GET/POST /link/:nonce | Per-user account-linking form (per-user mode only). |

Resuming a conversation. History persists automatically in per-user mode (keyed on the session cookie). In service mode (e.g. the admin chat), send a stable conversationId on POST /chat — a per-browser-session UUID works — and the transcript is saved to davepi's conversation collection under that id. To resume: GET /conversations to list, GET /conversations/:id to load the transcript, then continue by posting to /chat with the same conversationId. Omit conversationId and nothing is persisted server-side (the client round-trips history itself). Service-mode conversations are visible to any CORS-allowed caller (one shared service identity); per-user conversations are scoped to the linked user.

Configuration (env)

Required:

| Variable | Purpose | | ----------------- | ------------------------------------------------- | | DAVEPI_URL | Base URL of the davepi backend | | LLM_PROVIDER | anthropic (default), openai, openrouter, or ollama | | ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY | Provider key (not needed for ollama) | | LLM_MODEL | Model id. Optional for anthropic / openai / openrouter (sensible defaults); required for ollama |

Service auth (default) — set one of:

| Variable | Purpose | | ------------------ | ------------------------------------------------ | | DAVEPI_BEARER | Bearer credential for a davepi user. Use a dpk_ API key (long-lived, revocable) — a JWT works but expires (≤2h) and the agent won't rotate it, so a long-running agent dies. | | DAVEPI_CLIENT_ID | Public client id for anonymous reads |

Per-user auth:

| Variable | Purpose | | -------------------------- | ----------------------------------------------------------------------------- | | AGENT_AUTH_MODE=per-user | Switch on per-user mode | | AGENT_LINK_BASE_URL | Public base URL of the agent itself (where GET /link/:nonce is served) | | AGENT_SESSION_SECRET | Required for HTTP per-user mode — HMAC key for the signed session cookie | | AGENT_COOKIE_SECURE | true (default) — emits Secure on the session cookie. Set false for HTTP-only dev | | STORE_URL | Where to persist refresh tokens. file:./davepi-agent-store.json (default) or memory: |

Persona & memory (optional):

| Variable | Purpose | | ----------------------------------- | ------------------------------------------------------------------------------------------------ | | AGENT_KEY | Which agent this process is (e.g. support). Selects the agentPersona / agentMemory rows used as prompt slots. Unset → built-in default prompt and no persisted snapshot | | AGENT_PERSONA_CACHE_TTL_SECONDS | Per-process cache TTL for the persona lookup (default 60). Set 0 to fetch on every turn (strict immediacy) | | AGENT_PERSIST_CONVERSATIONS | Persist history + the frozen prompt snapshot to davepi's conversation schema (default true). false keeps the channel-managed in-memory round-trip only | | AGENT_SESSION_IDLE_SECONDS | Idle gap after which a returning user is a new session and the snapshot is re-frozen, picking up memory/profile writes from the prior session (default 1800) | | LLM_PROMPT_CACHING | Anthropic prompt caching on the frozen snapshot prefix (default true, Anthropic provider only). false to disable |

Models via OpenRouter

Set LLM_PROVIDER=openrouter to route through OpenRouter, a single API in front of hundreds of models from many vendors. The agent reuses the bundled @ai-sdk/openai provider pointed at OpenRouter's OpenAI-compatible /api/v1 endpoint, so there's no extra dependency — you just need an OPENROUTER_API_KEY.

| Variable | Purpose | | -------------------- | --------------------------------------------------------------------------------------------- | | OPENROUTER_API_KEY | Required — your OpenRouter key (sk-or-...) | | LLM_MODEL | OpenRouter vendor/model id (e.g. anthropic/claude-3.5-sonnet, openai/gpt-4o, google/gemini-2.0-flash-001). Defaults to openrouter/auto (OpenRouter picks a model per request) | | OPENROUTER_BASE_URL| Default https://openrouter.ai/api/v1. Override for a proxy | | OPENROUTER_SITE_URL| Optional — sent as HTTP-Referer for OpenRouter's leaderboard attribution | | OPENROUTER_APP_NAME| Optional — sent as X-Title for OpenRouter's leaderboard attribution |

LLM_PROVIDER=openrouter LLM_MODEL=anthropic/claude-3.5-sonnet \
  OPENROUTER_API_KEY=sk-or-... \
  DAVEPI_URL=http://localhost:5050 \
  DAVEPI_BEARER=dpk_... \
  npx davepi-agent

LLM_PROMPT_CACHING is a no-op for OpenRouter — prompt caching is applied only on the native Anthropic provider. Pick a model with solid tool-calling support (the big frontier models all qualify); the tool router keeps the exposed surface under AGENT_TOOL_LIMIT regardless.

Local models via Ollama

Set LLM_PROVIDER=ollama to run the agent against a local Ollama server. The agent reuses the bundled @ai-sdk/openai provider pointed at Ollama's OpenAI-compatible /v1 endpoint, so there's no extra dependency and no API key.

| Variable | Purpose | | ------------------ | ---------------------------------------------------------------------------------------- | | LLM_MODEL | Required — Ollama has no universal default. The exact name you ran ollama pull with (e.g. llama3.1, qwen2.5) | | OLLAMA_BASE_URL | Default http://localhost:11434/v1. Override for a remote Ollama or a reverse proxy | | OLLAMA_API_KEY | Optional — only set when fronting Ollama with an auth proxy. Ollama itself ignores it |

ollama pull llama3.1
ollama serve

LLM_PROVIDER=ollama LLM_MODEL=llama3.1 \
  DAVEPI_URL=http://localhost:5050 \
  DAVEPI_BEARER=<jwt> \
  npx davepi-agent

LLM_PROMPT_CACHING is a no-op for Ollama — prompt caching is an Anthropic feature only. Tool-calling fidelity depends on the model: llama3.1 and qwen2.5 work well with the MCP tool surface; smaller models may struggle on backends with many schemas (the tool router keeps the exposed surface under AGENT_TOOL_LIMIT).

Memory & the frozen snapshot

Once AGENT_KEY is set the agent reads a per-tenant memory row (agentMemory, slow-changing facts about the account) and a per-end-user profile row (customerProfile, preferences/notes — shared across the tenant's agents) and folds them into the system prompt alongside the persona. These five slots are snapshotted once at session start and frozen for the whole conversation, so the prefix stays byte-stable and Anthropic prompt caching keeps hitting. The agent self-authors memory/profiles through the schema-generated MCP tools (e.g. update_agentMemory); because the snapshot is frozen, a write takes effect on the next session, never mid-conversation.

Live vs. remembered. Treat memory/profile/persona as slow-changing background that may be slightly stale — never as live system state. For anything that changes (order status, ticket state, inventory, balances), the agent calls a tool to read it fresh. Snapshotted text shapes tone and recall; it is never an access-control or live-data mechanism (the JWT / client id remains the boundary).

Linking flow (per-user mode): on first contact from an unlinked user the agent returns a one-time link URL (<agent>/link/<nonce>). The user opens it, signs in via a small HTML form, and the agent calls davepi's POST /login server-side to obtain the refresh token. The refresh token never crosses the browser. For HTTP-channel users, link completion sets an HMAC-signed davepi_agent_session cookie (HttpOnly, SameSite=Lax) that /chat reads on every subsequent request. /chat ignores any caller-supplied channelUserId — the cookie is the trust boundary.

Channels:

| Variable | Purpose | | ----------------------- | ------------------------------------------------------ | | AGENT_HTTP_ENABLED | true (default) / false | | AGENT_HTTP_PORT | HTTP port (default 5070) | | AGENT_CORS_ORIGINS | Comma-separated allowlist | | SLACK_BOT_TOKEN | Enables the Slack channel when set | | SLACK_SIGNING_SECRET | Required when Slack is enabled (HTTP mode) | | SLACK_APP_TOKEN | App-level token for socket mode | | SLACK_SOCKET_MODE | true to use socket mode | | SLACK_PORT | Slack HTTP port (default 5061) | | AGENT_TRACE | Trace-log every turn's inputs, tool calls/results, and final output. On by default outside NODE_ENV=production/test; true/false to override |

Tools:

| Variable | Purpose | | ------------------------- | ---------------------------------------------------- | | AGENT_TOOL_LIMIT | Above this many tools, switch to routed mode (40) | | AGENT_INCLUDE_RENDER | Inject render_table + render_chart tools (true) |

Programmatic use

const { startAgent, runTurn, createAgent } = require('@davepi/agent');

// Start with all configured channels:
await startAgent();

// Or build the pieces and drive runTurn() yourself:
const { config, model, mcpClient, auth } = await createAgent({
  llm: { provider: 'openai', model: 'gpt-4o-mini' },
});
const out = await runTurn({
  config, model, mcpClient, auth,
  channelCtx: { channel: 'my-channel', channelUserId: 'user-123' },
  history: [],
  userMessage: 'Show me last week\'s orders as a chart',
  onEvent: console.log,
});

Slack setup checklist

  1. Create a Slack app at https://api.slack.com/apps.
  2. OAuth & Permissions scopes: app_mentions:read, chat:write, im:history, im:write, users:read.
  3. Event Subscriptions: enable; subscribe to app_mention and message.im.
  4. Install the app to your workspace; copy the Bot User OAuth Token to SLACK_BOT_TOKEN and the Signing Secret to SLACK_SIGNING_SECRET.
  5. For local dev without a public URL, set SLACK_SOCKET_MODE=true and provide SLACK_APP_TOKEN.
  6. Start the agent; @-mention it in a channel or DM it.

Telegram setup checklist

  1. Message @BotFather, send /newbot, and follow the prompts to name your bot.
  2. Copy the HTTP API token to TELEGRAM_BOT_TOKEN.
  3. Start the agent. It long-polls the Bot API — no public URL or webhook needed. DM the bot, or add it to a group and @-mention it.

Optional tuning: TELEGRAM_POLL_TIMEOUT (long-poll hold, default 30s), TELEGRAM_POLL_BACKOFF_MS (retry pause after a failed poll, default 2000), TELEGRAM_ENABLED=false to force the channel off even with a token set.

Proactive / scheduled agents (cron + attached skills)

Agents don't have to wait to be spoken to. Pair @davepi/agent with davepi-plugin-cron to run a fresh agent on a schedule that follows a named, approved skill (a governed runbook) and posts its output to Slack — follow-ups, SLA digests, end-of-day summaries.

const { createAgent } = require('@davepi/agent');
const cron = require('davepi-plugin-cron');

const agent = await createAgent({ agent: { key: 'support' } });

cron.register('daily-sla-digest', {
  schedule: '0 9 * * 1-5', // 9am on weekdays
  handler: agent.scheduledSkill({
    skill: 'Daily SLA digest',  // name of an *approved* skill for this agentKey
    slackChannel: 'C0123456789', // channel id to post into
    // prompt: 'optional override of the default autonomous preamble',
    // threadTs: 'optional thread to post into',
  }),
});

Each tick:

  1. Loads the named skill through the agent's own MCP identity, filtered to status: 'approved' — a draft/deprecated runbook is never fired.
  2. Runs a fresh runTurn (empty history, no end-user) with the persona loaded and the skill's body inlined as the task. Live data is fetched with tools, not assumed from the snapshot.
  3. Posts the reply (plus any render_table / render_chart output) to Slack.

Tenant scoping is inherited, not re-implemented: the agent's service auth owns exactly one tenant's data, so the skill lookup and the run are tenant-scoped server-side like every other read. For a multi-tenant deployment, register one job per tenant agent (each built with its own auth) or pass an explicit channelCtx.

Service auth is the default and the expectation. A scheduled run has no end-user, so the default cron context has no channelUserId. Per-user auth resolves the agent's identity from the end-user, so a per-user agent is rejected at registration unless you pass an explicit channelCtx with a channelUserId (advanced: a job that acts as one specific linked user).

SLACK_BOT_TOKEN must be set (the poster reuses the bundled @slack/web-api client); the full Slack channel doesn't have to be enabled. A run that produces no output skips the post. Cancellation is cooperative: the cron lease's AbortSignal is threaded into the run — forwarded to MCP tool calls and the model stream — so if the lease is lost mid-run (another node took over), in-flight tool calls and generation stop instead of continuing to write, and nothing is posted (no double-post). Use createScheduledHandler({ agent, ... }) directly if you'd rather not go through agent.scheduledSkill.

License

ISC