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

@alivelabs/sdk

v0.1.0

Published

Notify yourself and your team, and reach your people and your agents, from your own product.

Readme

@alivelabs/sdk

Tell yourself, or your team, what happened, in one line at the moment it happens: a build failed, a backup finished, a workout is done. What goes in a notification, and when, is your decision; how it reaches people is Alive's: by email, and by push on iOS.

  • Notify. From a script, a CI job, a cron line or your product's own code. It needs one key and nothing else.
  • People. The optional Guests layer for a product that has customers: put what one of your users does on their timeline in your organization's Guests, keyed by your own user id. An event can notify the organization's owners and can start one of your agents about that person.

No dependencies. It uses fetch, so it runs on Node 18 and later, Bun, Deno and edge runtimes. It is written in TypeScript and also works from CommonJS.

npm install @alivelabs/sdk

Notify yourself

In Alive, open Settings → Sessions → Personal API keys and create a key. Alive shows it once. Keep it in your environment as ALIVE_API_KEY, out of your source code and out of the browser.

import { Alive } from "@alivelabs/sdk"

const alive = new Alive({ key: process.env.ALIVE_API_KEY })

await alive.notify({ title: "Nightly backup done" })

That reaches you, by email and by push on iOS. A personal key acts as you, so there is nothing else to set up. Each person can turn these messages off under Settings → Notifications.

alive.notify(notification) returns { notified, reason? }.

| Field | | | --- | --- | | title | Required, up to 200 characters. | | body | A few more lines, up to 2000 characters. | | url | An http or https link, shown as the last line of the message. | | to | Emails of the people to tell instead of you. See Notify your team. | | id | Your id for the message. See Repeats and reminders. |

When notified is false, reason says why nobody was told: deduped (that id was already sent), suppressed (everyone it would reach has these messages switched off) or capped (the key's 100 a day are spent).

From a CI job or a shell

One curl, with the same key:

curl -X POST https://app.alive.site/api/notifications \
  -H "Authorization: Bearer $ALIVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Build failed","body":"main, 4 min","url":"https://ci.example/runs/42"}'

The answer is {"notified":true}. On a machine with the Alive CLI, the same call is one line, and it prints who was told or why nobody was:

alive notify "Build failed" --body "main, 4 min" --url https://ci.example/runs/42

The CLI reads ALIVE_API_KEY too, or the account you signed in with.

Notify your team

Name people with to, by the email of their Alive account:

await alive.notify({ title: "Pump 3 is offline", body: "Site 12, since 14:05", to: ["[email protected]", "[email protected]"] })

to takes the emails of people who share an organization with you, and nothing else: no ids, no guests, no customers. An email that belongs to no one you share an organization with is refused with a 400 that names it, and nothing is sent to anyone on the list. Naming people replaces you, so add your own email to to to be told as well.

From an agent

An agent in Alive reaches you with its notification_send ability, in every run. It needs no key and no SDK.

Your product's key

A product that has its own organization uses a key made for it. Open Guests in Alive, choose Product API, and create a key for your product. Alive shows the key once. When you choose a workspace for it, Alive saves it there as ALIVE_INGEST_KEY, so your chats and your deployed product can read it from the environment. Keep it out of your source code and out of the browser.

const alive = new Alive({ key: process.env.ALIVE_INGEST_KEY })

await alive.notify({ title: "Leg day done", body: "4 of 5 sets, 52 min", url: "https://gym.example/today" })

notify takes the same fields and answers the same way. The difference is who it reaches: a notification with no to reaches the owners of your organization, and to takes the emails of members of your organization, whatever their role. Naming members replaces the owners, so add an owner's email to to to tell them as well. An email that belongs to no member is refused with a 400 that names it. Each person can turn the messages off under Products in Settings → Notifications.

A key belongs to one organization and writes under one product name. Revoke it in the same place and every call that uses it stops.

A product's key also unlocks People, below. A personal key does not: alive.people throws "People needs your product's key, from Product API in Guests." before it sends anything.

People: record what a user did

For a product with customers, your user id is the person's identity in Alive. Send it as external_id, and the first event about a user creates them. There is nothing to create first:

await alive.people.event({
  contact: { external_id: user.id, email: user.email },
  event_id: `signup:${user.id}`,
  label: "Signed up",
  detail: "Pro, monthly",
  tone: "positive",
  notify: true,
})

email is optional. Until you send a name, the person is shown under their external_id.

alive.people.event(event)

Adds one event to a person's timeline. Returns { recorded, run? }.

| Field | | | --- | --- | | event_id | Required. Your id for the event. Sending it again is acknowledged and adds nothing, so a retry is safe. | | contact | Required. { external_id }, { email } or both. A person Alive does not know yet is created from the external_id or the email. An external_id that already belongs to another person is refused with a 409. | | label | Required. What happened, in a few words. | | detail, tone | A sentence of detail, and positive, neutral or attention. | | occurred_at | When it happened, as an ISO time. A time still to come is stored as the time the event arrived. | | notify | true tells your organization's owners. A repeat of the event tells nobody. | | run | { agent_id } starts that agent's run about the person. The agent must belong to your organization and have a connected product as its trigger. A repeat of the event does not start a second run. |

result.run.status says what became of the run: retained when the agent has it, daily_limit or inbox_full when the agent could not take it yet. Send the event again later and Alive tries again.

alive.people.upsert(person)

For a product that wants the name and details right. Creates the person, or updates them when their external_id, or their email, is already in Guests. Returns { person_id, created }.

await alive.people.upsert({ external_id: user.id, email: user.email, name: user.name, attributes: { plan: "pro" } })

| Field | | | --- | --- | | email, name | Required. | | external_id | Your own id for the person. Alive looks it up first, so a person who changed their email stays one person. | | phone, title | Optional. | | attributes | Up to 50 flat values: text, numbers or true/false. Send null to remove one. For anything nested, send a JSON string. | | subscribed | Only false. It records an opt-out and never subscribes anyone again. |

alive.people.delete({ external_id })

For an account deletion. Deletes the person and their timeline. Name them by external_id, by email, or both.

await alive.people.delete({ external_id: user.id })

Repeats and reminders

The same id is delivered once. A second call with an id Alive already has tells nobody and answers { notified: false, reason: "deduped" }, so a job that retries, or runs twice, does not tell you twice. A daily reminder sends one id per day:

await alive.notify({ title: "Water the plants", id: `water-${siteId}-${date}` })

Without an id, every call notifies once, retries included: the package gives each call an id of its own. A key reaches at most 100 messages a day (per UTC day). After that notified is false with reason: "capped" until the day turns over; a repeat or a refused call never counts toward it.

Errors

Every call that fails throws an AliveError. Its class says what kind of failure it was, so you branch on instanceof instead of on status codes:

AliveError
├─ AliveAuthenticationError   401: the key is wrong or has been revoked
├─ AlivePermissionError       403: the key may not do this
├─ AliveInvalidRequestError   any other 4xx: the call is wrong, or names something that is not there
├─ AliveRateLimitError        429: the key used its 600 calls a minute
├─ AliveAPIError              5xx, 408, or an answer that is not what Alive sends
└─ AliveConnectionError       no answer at all
   └─ AliveTimeoutError       no answer in time
import { Alive, AliveError, AliveRateLimitError } from "@alivelabs/sdk"

try {
  await alive.notify({ title: "Leg day done" })
} catch (error) {
  if (error instanceof AliveRateLimitError) console.error(`Try again in ${error.retryAfterSeconds} seconds`)
  else if (error instanceof AliveError) console.error(error.status, error.code, error.message, error.requestId)
  else throw error
}

| Field | | | --- | --- | | message | What happened and what to do. | | code | Alive's code, such as UNAUTHORIZED or AGENT_NOT_FOUND; HTTP_<status> for an answer without one; CONNECTION_ERROR or TIMEOUT when no answer came. | | status | The HTTP status, or undefined when no answer came. | | requestId | Alive's id for the request. Quote it when you ask about a failed call. | | headers, body | The answer's headers, and its body, parsed when it was JSON. | | retryAfterSeconds | How long Alive asked you to wait, when it said. | | cause | For a connection error, what fetch threw. |

instanceof holds across copies of the package: an app that loads both the ESM and the CommonJS build, or two versions, can test an error from one against the classes of the other.

A mistake in your own code throws before anything is sent, as a plain TypeError, RangeError or Error: no key, a key that starts with neither alive_ nor alv_ik_, an option the package does not know, or alive.people with a personal key.

Retries

A failure that may pass is tried again, twice by default: no answer, a timeout, a 408, a 429 or a 5xx. A 400, 401, 403, 404, 409 or 422 is not, since the same call gets the same answer. Where Alive's answer says whether trying again helps, that decides. Between attempts the package waits half a second, doubling up to five, with a random part so many clients do not all come back at once. When Alive answers with Retry-After, it waits that long instead, up to a minute. A longer wait ends the call with that error, and retryAfterSeconds tells you when to come back.

A retry never does anything twice: a notification carries an id, an event its event_id, and an upsert or a delete changes nothing the second time. When an attempt's answer was lost, the retry's answer can read differently from the first attempt's:

  • notify without your own id answers { notified: true }, because the lost attempt notified. With your id it answers deduped, since an earlier call of yours may have sent that id.
  • people.event can answer recorded: false, and people.upsert can answer created: false, for what the lost attempt did.
  • people.delete resolves when the retry finds the person already gone. On a first attempt, a person nobody knows is an AliveInvalidRequestError with status 404 and code NOT_FOUND.

Options

const alive = new Alive({
  key: process.env.ALIVE_API_KEY,
  url: "https://app.alive.site", // where Alive runs
  maxRetries: 2, // from 0 to 10
  timeout: 30_000, // milliseconds each attempt waits for the whole answer
  fetch, // your own fetch, for a proxy or a test
})

key is required: a personal key (alive_…) or a product's key (alv_ik_…). The constructor reads which it is once. Everything else is optional, with the defaults above.

Every call takes options of its own as its last argument, over the client's:

await alive.notify({ title: "Deploy done" }, { timeout: 5_000, maxRetries: 0 })

const controller = new AbortController()
const sent = alive.people.event(event, { signal: controller.signal })
controller.abort() // the attempt in flight stops, no retry starts, and `sent` rejects with the signal's reason

VERSION is the package's version; every request sends it in its User-Agent.

Without the SDK

The package is a thin layer over HTTP. Every request carries your key as Authorization: Bearer <key> and a JSON body. A personal key notifies with POST /api/notifications, shown above.

Notify with a product's key (the organization's owners; add a to array of member emails to name others):

curl -X POST https://app.alive.site/api/crm/ingest/messages \
  -H "Authorization: Bearer $ALIVE_INGEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"message_id":"leg-day-2026-10-30","title":"Leg day done","body":"4 of 5 sets, 52 min","url":"https://gym.example/today"}'

The answer is {"notified":true}, or {"notified":false,"reason":"deduped"} for a message_id Alive already has. message_id is your id for the message and is required here; body and url are optional.

Record an event about a user (a product's key):

curl -X POST https://app.alive.site/api/crm/ingest/events \
  -H "Authorization: Bearer $ALIVE_INGEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contact":{"external_id":"user_123","email":"[email protected]"},"event_id":"signup:user_123","label":"Signed up","detail":"Pro, monthly","tone":"positive","notify":true}'

The answer is {"recorded":true}. People are saved with PUT /api/crm/ingest/contacts and deleted with DELETE /api/crm/ingest/contacts?external_id=user_123, with the fields described above.

Publishing a new version

For the maintainers. Raise version in package.json and in src/version.ts, add the release to CHANGELOG.md, then, from the repository root:

cd packages/sdk && npm login && bun scripts/publish.ts

The script builds into a fresh folder, writes the manifest a customer installs (no scripts, no development dependencies, nothing from the workspace) and refuses a version npm already has. bun scripts/publish.ts --dry-run lists what would be published and publishes nothing. A bare npm publish in this folder refuses: its package.json is the repository's own.