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

@peeve/wizard

v0.11.3

Published

The Peeve setup wizard — npx @peeve/wizard detects your framework and installs the Peeve widget/SDK with your publishable key, safely.

Downloads

399

Readme

@peeve/wizard

Install the Peeve widget/SDK into your app with one command — modeled on @posthog/wizard. It detects your framework, injects the real Peeve snippet with your publishable key in the right file, shows you a diff, backs up everything it touches, and verifies the snippet landed.

npx @peeve/wizard

Two paths. A deterministic, offline, rules-based path covers the 80% case (Next.js, React + Vite, Vue, plain HTML). When your project is unusual — an unknown framework, or a known one whose entry file can't be edited safely — the wizard falls to the AI-assisted path: it sends the candidate file(s) to Peeve's backend, Claude returns the edit, and it lands through the exact same safety (diff, confirm, .bak backup). Browser device-login key fetch and a live first-ping poll are still a later phase.

What it does

  1. Requires a clean git tree so every change is reversible with git checkout (override with --yes / --force; if it isn't a git repo it warns and still writes .bak backups).
  2. Detects your framework from package.json + key files.
  3. Asks for your publishable key (pk_live_…) — or takes --publishable-key. A secret sk_… key is rejected; it must never live in your app.
  4. Injects the real Peeve snippet in the right place, writes the key to .env.local as a public env var (never inlined, except plain HTML), shows a diff, asks for confirmation, and backs up every modified file to <file>.bak. Runs are idempotent — an existing Peeve snippet is detected and skipped, never duplicated.
  5. Sets up the halves your project actually has — the browser widget where there's a UI, the server SDK where there's a real backend, both where there are both — and wires both identify calls: the server one where your app reads the session (not where it signs someone in), and the browser one where your pages know who is signed in.
  6. Offers to install @peeve/sdk with your own package manager, detected from your lockfile. It shows the exact command and asks separately — this is the only thing it executes.
  7. Marks your pricing plans and cancel controls with Peeve's data-peeve-* attributes — see billing annotations.
  8. Verifies the snippet is present on disk and tells you exactly how Peeve confirms the first real ping (Widget → Verify in the dashboard).
  9. Ends with what's left for you — anything it printed instead of wiring, the secret key, an install you declined — repeated after the summary so it can't scroll away. See what's left for you.

Supported frameworks

| Framework | File it edits | What it inserts | |---|---|---| | Next.js App Router | app/layout.tsx | creates PeeveInit.tsx (a "use client" component that calls Peeve.init()), renders <PeeveInit /> in <body> | | Next.js Pages Router | pages/_app.tsx | creates it if missing, else adds Peeve.init() in a useEffect | | React + Vite | src/main.tsx | import { Peeve } from "@peeve/sdk" + Peeve.init() at startup | | Vue | src/main.ts | Peeve.init() before mount() | | Plain static HTML | index.html | <script src="…/widget.js" data-publishable-key … defer> before </body> |

Env vars written: NEXT_PUBLIC_PEEVE_PUBLISHABLE_KEY (Next), VITE_PEEVE_PUBLISHABLE_KEY (Vite/Vue), or plain PEEVE_PUBLISHABLE_KEY for a backend with no browser framework to prefix it. Force detection with --framework next-app|next-pages|react-vite|vue|html.

One env file per run. The publishable key and the secret key's name land in the same file — .env.local, or .env when there's a bare Node server, which is the only one dotenv reads by default and which every supported browser framework reads too. They used to be chosen separately, by the browser framework and the server framework, so a Vite front end with an Express backend could scatter them across two files, and a backend-only project got no publishable key written anywhere at all.

The one command it runs

Adding @peeve/sdk to your package.json is the only thing the wizard needs to execute rather than write. It detects your package manager from the lockfile (package-lock.json → npm, pnpm-lock.yaml → pnpm, yarn.lock → yarn, bun.lockb → bun; Corepack's packageManager field if there's no lockfile yet; npm otherwise), shows you the exact command, and asks separately:

@peeve/sdk isn't installed here yet. (pnpm-lock.yaml → pnpm)
Run `pnpm add @peeve/sdk` now? › yes / no
  • --yes does not answer this. Approving the edits this package authored is a different question from executing code on your machine right now — an install runs every lifecycle script in the dependency graph. Neither does a non-interactive run.
  • --dry-run prints the command and runs nothing.
  • Declining is a complete outcome, not a failure: you get the command, and the install lands when you run it. Same if the package manager errors, or isn't on your PATH — a warning and the command, never a failed run.
  • It never writes your package.json. The package manager owns that file, and the wizard refuses it as an edit target for good reason.
  • Skipped entirely when @peeve/sdk is already a dependency (which is what makes a second run a no-op), and for a plain-HTML <script> install, which needs no package.

Server-side setup (@peeve/sdk/server)

The widget is only half of Peeve. The other half runs on your server: it identifies a signed-in user server-to-server and hands Peeve that user's API token for your own product — the credential the agent uses when it acts on their behalf, and the one thing that must never travel through a browser.

The wizard sets it up only where a backend really exists, because a dependency is not evidence: every Next.js project depends on next, and most of them have never written a route handler. It looks for app/api/, pages/api/, server/, src/server/, route handlers (route.ts, +server.ts, *.server.ts), or a server entry file, across Next.js, Nuxt, SvelteKit, Remix, Express, Fastify and Hono.

So each project gets what it is:

| Project | Browser widget | Server SDK | |---|:--:|:--:| | Next.js with app/api/ | ✅ | ✅ | | Next.js with no backend in it | ✅ | — | | Vite SPA | ✅ | — | | Express API with no UI | — | ✅ |

What it writes is a small module (lib/peeve.ts, server/utils/peeve.ts, src/lib/server/peeve.ts, app/peeve.server.ts or src/peeve.ts, depending on the framework) holding your PeeveServer client and a worked identify:

import { PeeveServer } from "@peeve/sdk/server";

export const peeve = new PeeveServer({ secretKey: process.env.PEEVE_SECRET_KEY! });

const u = await peeve.identify({ userId: user.id, token: user.apiToken });

identify goes where the session is READ

A module nothing calls identifies nobody, so the wizard also finds the place that call belongs — and that place is not your sign-in handler. A sign-in is an event, and almost no request is that event: someone signs in on Monday and reloads your app every day that week. On every one of those loads they are signed in, no sign-in handler runs, and identify would never fire again.

The mapping Peeve needs is per-request state, so the call goes wherever your app resolves the current user — once per request, or once per render:

| Framework | Where the call goes | Why there | |---|---|---| | Next.js App Router | the root app/layout.tsx | a server component: it renders on the server for every request, and every route passes through it | | Next.js Pages Router | there is no per-request server root — _app runs in the browser. The wizard says so and points at where the session really is resolved: getServerSideProps, your auth helper, or the auth callback | inventing a root that doesn't exist would be worse than naming the one that does | | SvelteKit | hooks.server.ts → handle | every request runs through it, and it's where event.locals.user is populated | | Nuxt | a server/middleware/ handler | Nuxt runs each of them on every request | | Remix | the root loader in app/root.tsx | runs on every document request and every navigation | | Express / Fastify / Hono | middleware or a preHandler hook, after your session middleware | that is when req.user exists |

Knowing the file is not knowing how your codebase gets the current user — event.locals.user, await auth(), getServerSession(authOptions), a bespoke cookie parser. Only your code knows, so the wizard asks Peeve to make the edit (the same AI path, with the same diff, confirmation and .bak backup) and, when it can't — no terminal, --yes, --dry-run, an unreachable planner, or no safe edit — prints the exact file and the exact call instead of guessing at your auth flow. Printing is a complete outcome here, not a failure.

The generated identifyUser() is built to survive being called on every render: it returns immediately when nobody is signed in, keeps one client per process, collapses repeat calls for the same user into one network round trip, and never throws — identifying a user is not worth failing someone's page load over. A project that already calls identify is left alone, so a second run adds nothing.

The browser half: Peeve.identify()

identifyUser() is only one of the two. It is server-to-server and trusted: it stores that user's token for your product. It tells the widget nothing — the widget runs in a page your server never speaks to.

Peeve.identify({ userId, email, name }) is the other call, in the browser, and it is deliberately untrusted. All it does is say who is looking. The widget gates its /agent command on having a user id, and that id comes from this call and nowhere else — so a workspace with a perfectly good stored token still shows the agent to nobody until the page says who is there. Both are required; the wizard wires both, in one pass.

It does not go next to Peeve.init(). The entry file the wizard already edits (PeeveInit.tsx, src/main.tsx) is exactly the file with no session in scope — that is why init() lives there. So the browser call goes where the user is: your session hook or provider. Which file that is cannot be read off a path (components/AuthProvider.tsx in one app, app/providers.tsx in the next), so the wizard reads the files instead, and shapes the call for the library it finds:

| Library | Read as | The id | |---|---|---| | NextAuth / Auth.js | useSession() | session.user.id — only if your session callback adds it, and it must match the server's | | Clerk | useUser() | user.id, with primaryEmailAddress and fullName | | Auth0 | useUser() / useAuth0() | user.sub | | Supabase Auth | onAuthStateChange() | user.id, name from user_metadata | | Firebase Auth | onAuthStateChanged() | user.uid, name from displayName | | your own hook / store | useAuth() | user.id |

The mapping is the part worth being exact about: a generic user.id example is wrong for Auth0 and Firebase, and Peeve.identify({ userId: undefined }) identifies nobody while looking exactly like a working install.

The call is written as an effect (or the library's own subscription callback), so it never changes what your component returns, returns early when nobody is signed in, and is safe on every render. Alongside it the wizard leaves a commented Peeve.setContext({ seats: 12, role: "admin" }) — the free-form business context the agent sees. Its example never uses email, name, company, phone, plan, createdAt, userId or token: those belong to identify() and setContext drops them silently, so a first call using one looks like it works and does nothing.

When the wizard can't find where your pages know who is signed in, it prints the exact call and says so — same posture as the server half, and it uploads nothing. A project with no sign of signed-in users at all is told the call is for when it has them, and nothing is listed as outstanding.

What's left for you

Every step that can't finish on its own explains itself where it happens — and then the run keeps going, and the explanation scrolls away. So the wizard ends by repeating them, after the summary, numbered, each with the file and the exact line:

3 things left for you

  1. @peeve/sdk isn't installed here yet — the code the wizard wrote imports it.
     Run    npm install @peeve/sdk

  2. The server half can't authenticate until PEEVE_SECRET_KEY has a value in .env.
     File   .env
     Paste  PEEVE_SECRET_KEY=sk_live_…  (app.peeve.ai → Settings → API keys)

  3. src/peeve.mjs was created, but nothing calls identifyUser() yet.
     File   src/server.js
     Add    await identifyUser(req.user)

Entry 3 is the one this exists for: a module in your repo that nothing imports is worse than no module, because you can't tell why it's there. A run with nothing outstanding prints nothing extra.

The secret key stays yours

The wizard never accepts, prints, or writes a secret key. It writes the env var name — PEEVE_SECRET_KEY=, with an empty value and a comment — into .env.local (or .env for a bare Node server), and tells you to paste the value in yourself. --publishable-key sk_… is still rejected outright, and if the env file isn't gitignored the wizard says so before you paste anything into it.

Billing annotations

In the same run, the wizard looks for your pricing plans and subscription-cancel controls and marks them with the attributes Peeve reads:

| Attribute | Means | |---|---| | data-peeve-cancel | this is a cancel/downgrade control (presence only, no value) | | data-peeve-plan="Growth" | the plan's name | | data-peeve-amount="99" | the price exactly as your page shows it — never reformatted | | data-peeve-interval="monthly" | monthly, quarterly or yearly | | data-peeve-currency="EUR" | only when it isn't USD |

It finds the candidate files by path — pricing, plans, billing, subscription, checkout, upgrade, settings, account — capped at 6 files, skipping API routes, tests, and anything already carrying data-peeve-, which is what makes a second run a no-op. The AI path does the editing (pricing markup is written a thousand different ways), so every annotated file gets its own diff, confirmation and .bak backup, exactly like an install edit.

Finding nothing is the normal outcome for most projects. The wizard says so in one line and moves on — it never fails the run.

AI-assisted install (non-standard projects)

If the deterministic path can't classify your project — an unknown framework, or a known framework whose entry file it can't edit safely — the wizard asks Peeve's backend for help instead of giving up:

  1. It gathers up to 3 candidate files (the detected entry, then the usual suspects: app/layout.*, pages/_app.*, src/main.*, src/index.*, index.html, …), each capped to ~40 KB.
  2. It sends them to Peeve, authenticated with your publishable key as a bearer token — the same pk_… the wizard already collects. No secret key is ever sent, and no source is stored server-side. The request carries the framework, the canonical Peeve snippet, the env-var name, and the candidate files.
  3. Claude (via Peeve's Vercel AI Gateway) returns whole-file edits. The wizard diffs each one against the file on disk, asks you to confirm, backs the file up to <file>.bak, and only then writes — identical safety to the deterministic path. It never applies an edit blind and never fabricates one.
  4. If Peeve can't produce a safe edit (it returns ok:true with no edits), is unreachable, or errors, the wizard prints its reasoning and falls back to the copy-paste snippet — never worse than the manual flow, never a crash.

Use --ai to force this path even when deterministic detection would work.

Options

--publishable-key <pk_...>  Your publishable key. Prompted in a TTY. (Alias: --key)
--framework <name>          Force detection.
--ai                        Force the AI-assisted path (ask Peeve to plan the
                            install), even when deterministic detection would work.
--dir <path>                Project directory to scan + edit (default: cwd).
--dry-run                   Show what would change; write nothing, run nothing.
--no-verify                 Skip the post-install on-disk check.
-y, --yes                   Don't prompt: proceed past a dirty tree and apply the
                            DETERMINISTIC install. Does NOT approve AI edits, and
                            does NOT run your package manager.
--force                     Proceed even if the git tree is dirty.
-h, --help                  Show help.
-v, --version               Print the version.

The Peeve platform URL is fixed — there's no base-URL flag or env var to set.

--yes does not approve AI edits

--yes applies the deterministic install without prompting, and --ai --yes is refused outright. The two paths are not the same trust question:

  • The deterministic path writes snippets this package authored — they live in src/snippets.ts, readable before you ever run the wizard. --yes there is a decision you can make in advance.
  • The AI path writes whole files authored by a remote service. The diff is the only review they get, so a person has to see it. A security audit of this package found two routes to code execution on a developer's machine that a returned edit could take — a .git/hooks/pre-commit, and a package.json postinstall. Both are refused outright now, but a denylist only covers what someone thought of; the person reading the diff covers the rest.

To review an AI plan without an interactive terminal, use --dry-run — it prints the same diffs and writes nothing.

Safety model

  • Git-clean precondition — refuses a dirty tree without --yes/--force, and refuses just as firmly when it can't tell whether the tree is clean.
  • Diff + confirm — nothing is written without a shown diff and a yes. The diff escapes control characters, so file contents can't move the cursor or erase what you just read, and it's length-capped, so nothing scrolls the real change away.
  • Backups — every file that already exists is copied to <file>.bak before writing, and a second run never overwrites the first backup (.bak.1, .bak.2, …).
  • Additive & idempotent — only inserts imports / an init call / a render / a script tag; never rewrites unrelated code; never double-installs.
  • Publishable key only — pk_… is public by construction; a secret sk_… key is rejected outright and never written anywhere. The server half writes the env var NAME (PEEVE_SECRET_KEY=) and nothing else; the value is yours to paste.
  • Never worse than manual — for an unrecognized project it prints the exact snippet and the file to paste it into, rather than guessing. Anything it printed instead of wiring is repeated at the end, so a complete outcome is never one you had to scroll back to find.
  • AI edits get the same gate — the AI path returns whole files, but each one is still diffed against disk, confirmed, and backed up before a byte is written, and the confirm prompt says the changes came from the planner.
  • A returned path is treated as hostile input — it is resolved through symlinks against the real filesystem (a symlinked directory inside the project does not count as inside it), and anything that executes without you running your own app is refused: .git/, node_modules/, package.json, .npmrc, .husky/, CI and editor config. The response itself is size-capped, and two edits claiming the same path collapse to one so a later write can't contradict a diff you already approved.
  • Exactly one command is ever executed, and only if you say yes. Nothing the wizard writes runs without you running your own app; the single exception is your package manager installing @peeve/sdk, which is asked and answered on its own. The exact command is shown first, --yes does not approve it, a non-interactive run does not either, --dry-run only prints it, and declining leaves the command for you to run. It is spawned without a shell, with a fixed argument list, in your project directory — and it, not the wizard, is what writes package.json.

What's deferred

  • Browser device-login to fetch the workspace's publishable key automatically (today you paste pk_… or pass --publishable-key).
  • Live first-ping poll — watch the install signal and print a green check the moment the widget's first real page load reaches the platform.

Development

npm install
npm run build      # tsc → dist/
npm run typecheck  # tsc --noEmit
npm run smoke      # build fixtures + real CLI runs

License

MIT © Peeve.ai (owned by Veltron LLC)