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

@pushary/eve

v0.3.4

Published

Phone approvals for Eve agents. Your agent asks, your user taps Approve or Deny.

Readme

@pushary/eve

Phone approvals for Eve agents. Your agent asks, your user taps Approve or Deny.

Full walkthrough: Human-in-the-loop for Eve

CI npm license

What you need

Quick start

npm i @pushary/eve

Eve discovers tools by file. Drop in two one-line files:

// agent/tools/ask-human.ts
import { pusharyAskHuman } from '@pushary/eve'
export default pusharyAskHuman()
// agent/tools/connect-phone.ts
import { pusharyConnectPhone } from '@pushary/eve'
export default pusharyConnectPhone()

That is it. The agent now has ask-human (approve, choose or type an answer on a phone, and wait for it) and connect-phone (returns the connect link).

Two calls is the whole integration: pusharyConnectPhone() returns a link the end-user opens once to connect their phone, and pusharyAskHuman() asks that person and waits for the answer. No answer means no.

Eve's own approval docs: Multi-tenant approvals.

Gating a tool the model cannot skip

pusharyAskHuman() is a tool the model chooses to call. That is right for "go ask someone about this", and wrong for "this must not happen without a yes", because a model that does not want to be interrupted can decline to call it.

For an enforced gate, use pusharyApproval() in Eve's own per-tool approval: field. Eve evaluates it before execute runs, so there is no path around it:

// agent/tools/issue-refund.ts
import { defineTool } from 'eve/tools'
import { z } from 'zod'
import { pusharyApproval } from '@pushary/eve'

export default defineTool({
  description: 'Refund an order',
  inputSchema: z.object({ amount: z.number(), customer: z.string() }),
  approval: pusharyApproval(),
  execute: async ({ amount }) => refund(amount),
})

Eve names a tool by its file path, so there is no name field; this one registers as issue_refund.

It sits alongside Eve's built-in always(), never() and once(), which decide statically. This one asks a person and waits for the answer.

Fail-closed: a denial, an expiry, or nobody answering all come back denied and the tool does not run. The decision is keyed on session, call and tool, so a replayed turn resolves to the same decision instead of asking twice.

For a multi-tenant product, the person who answers is the signed-in user of the session. That is the default, and you can spell it out:

approval: pusharyApproval({ externalId: (ctx) => ctx.session.auth.current?.principalId })

Never take the end-user from ctx.toolInput. The model writes the tool input, so a prompt injection could send the approval to a different customer. If your id for the user lives somewhere else, look it up on your server from the session.

pusharyApproval<TInput>() is generic over your tool's input, so ctx.toolInput is typed inside a question builder.

The channel

The tools above are for asking on purpose. The channel covers everything Eve already pauses on: any tool gated with approval from eve/tools/approval, and the built-in ask_question. Eve renders those as buttons in Slack. This renders them on a phone.

// agent/channels/pushary.ts
import { pusharyChannel } from '@pushary/eve'
export default pusharyChannel()
PUSHARY_API_KEY=pk_...sk_...
PUSHARY_WEBHOOK_SECRET=whsec_...          # decisions.getWebhookSecret()
PUSHARY_CALLBACK_ORIGIN=https://your-agent.vercel.app

Eve emits input.requested and parks the turn durably. The channel opens a Pushary decision carrying a signed callback URL, and nothing is held open while it waits, so an approval can sit for hours at zero idle compute. When the human taps, POST /pushary/answer verifies the webhook signature and the per-request routing signature, then resumes the parked turn with the matching inputResponses entry.

Three more routes put the rest of the session on the phone:

| Route | Does | | --- | --- | | POST /pushary/message | Send a follow-up, starting a session if there is none | | POST /pushary/stop | cancel() the in-flight turn, leaving history intact | | POST /pushary/reset | reset() the session so the next message starts clean |

Options are matched back by label and then by id, so an approval resolves to Eve's approve or deny and a select resolves to the option that was tapped. Each decision carries an idempotency key derived from the session and request id, so a replayed step never asks the same person twice. A stale answer, one whose session already moved on, returns 410 rather than failing the webhook.

Who answers

By default each tool asks the session principal, so run user-scoped auth and each end-user is their own principal. To bind a fixed end-user (single-user agents, jobs), pass one:

export default pusharyAskHuman({ externalId: 'user_123' })

If there is no principal and no configured externalId, the tool throws a clear error instead of asking the wrong person.

Behavior that matters

  • Fail-closed. A declined, expired, or unanswered confirm is reported to the model as "not approved, do not proceed." Approval only happens on an explicit yes.
  • Serverless-safe. Each ask blocks up to 55 seconds by default (timeoutMs). The decision stays answerable for its full lifetime.

Config

pusharyAskHuman(config?) / pusharyConnectPhone(config?) accept { apiKey?, externalId?, agentName?, timeoutMs?, baseUrl? }. apiKey defaults to process.env.PUSHARY_API_KEY.

Under the hood

Thin binding over the shared adapter kernel in @pushary/server (@pushary/server/adapters), which every Pushary framework adapter is built on. The tools use enroll + decisions.ask; the channel uses decisions.create with a signed callback URL; pusharyApproval() uses the kernel's fail-closed gate. Requires [email protected] or newer. See the adapters guide.

MIT

Example

A runnable example is in examples/.