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

handraise-telegram

v0.1.0

Published

Telegram channel for handraise: an agent's approval arrives as a screenshot with Approve/Deny buttons, and the tap settles the handoff.

Readme

handraise-telegram

Your agent's approval arrives in Telegram as a screenshot with two buttons — and the tap is the answer.

npm install handraise handraise-telegram

The adapter is not on npm yet. handraise is: channels, which this package is built on, landed in handraise 0.5.1. Until the adapter is published, install handraise from npm and take this one from the repo — see Version note.

import { raiseHand } from "handraise"
import { telegram } from "handraise-telegram"

const { TELEGRAM_BOT_TOKEN = "", TELEGRAM_CHAT_ID = "" } = process.env

// Build it once and reuse it: one channel owns one poll loop. See below.
const channel = telegram({ botToken: TELEGRAM_BOT_TOKEN, chatId: TELEGRAM_CHAT_ID })

await raiseHand(page, {
  mode: "approval",
  reason: "The agent may not move money without a human",
  action: "Submit $12,430 vendor payment to Acme GmbH",
  channels: [channel],
})
// outcome "approved" → somebody pressed Approve, in Telegram or on the phone

That is the whole integration. telegram() returns a HandoffChannel; handraise calls it when the handoff starts and hands it the screenshot the phone would show, plus a way to answer.

Zero dependencies. fetch, FormData and Blob are globals on Node 20+ and on bun. The one thing it imports is node:crypto, for the random nonce that authorizes a button press.

Setup, in two minutes

  1. Message @BotFather, send /newbot, copy the token.
  2. Send your new bot a message (a bot cannot write to you first), then open https://api.telegram.org/bot<TOKEN>/getUpdates and read chat.id out of the answer. For a group, add the bot to it and do the same.
  3. Put both in your environment.

No webhook, no public URL, no ngrok: this package long-polls getUpdates, so it only ever makes outbound requests. (That is why Telegram is the first adapter; Slack needs a public interactivity endpoint.)

What the human sees

An approval is one photo — the page as the agent left it — captioned with the reason and the exact step, with Deny and Approve under it. One tap settles the raiseHand call that is waiting. The buttons then disappear and the caption gains a line:

The agent may not move money without a human

Submit $12,430 vendor payment to Acme GmbH

Approved by Simon

If the phone got there first, the tap says so instead — "Already decided elsewhere — this tap changed nothing" — and nothing is overturned. handraise's first answer wins, whoever gives it.

A takeover is a message with the reason and the handoff link, because a takeover cannot be answered from a chat: the human has to drive the browser, and only the handoff page can do that.

Options

| Option | Type | Default | | |---|---|---|---| | botToken | string | required | From @BotFather. A credential — it is in every request URL. | | chatId | string \| number | required | Numeric chat id, or @channelname. | | pollIntervalMs | number | 25 s | How long one getUpdates call waits for a press before it is made again (Telegram's timeout). Must be positive and finite, at most 50 s — Telegram's own cap. | | maxWaitMs | number | 6 min | How long to keep an approval open. Must be positive and finite, at most 24 h. See the limitation below. | | baseUrl | string | https://api.telegram.org | The Bot API root. | | onWarn | (event, detail) => void | — | Called for every failure this package swallows — a chat call Telegram refused while catching up with a decision already made. detail has the token redacted. Nothing is printed without it. |

When an approval closes

It closes when the handoff does. handraise 0.5.1 hands every channel a settled promise, and this package waits on it alongside the button. So an approval answered on the phone, or one that times out, or one whose browser session dies, closes here immediately: the buttons come off and the caption says which of those happened — "Decided on the phone: approved.", "Timed out — nobody answered in time.", "The browser session ended before this was answered."

And nothing is left running. The getUpdates call in flight is dropped when the last approval closes, not waited out, so this package lets go of your event loop instead of holding it for the rest of pollIntervalMs — 25 seconds by default. scripts/exit-probe.ts measures exactly that, as a test: a child process that settles a handoff and then does nothing exits 1 ms after notify returns. Before the poll was aborted it exited 25.0 s after.

maxWaitMs is only a backstop. It bounds the wait for a handraise that somehow never settles, which should not happen. It matters in one case: if you raise timeoutMs on raiseHand past 6 minutes, raise maxWaitMs to match, or the buttons go away while the agent is still waiting.

One sharp edge remains, and it is small: a press that lands in the same instant the approval closes is consumed but not acted on, so that person's button spinner hangs until Telegram gives up on it. The handoff is unaffected.

One process per bot, and that one is fine. getUpdates is a bot-wide stream with a single cursor: an update is confirmed for the whole bot the moment any call uses a higher offset, and Telegram answers 409 to a second overlapping call. So:

  • Inside one process, any number of approvals can be open at once. One telegram({ … }) owns one long-poll loop and hands each press to the approval whose button it is. Approvals from separate telegram() calls in the same process are separate loops and will conflict — build the channel once and reuse it.
  • A second process on the same token is a conflict. Both get 409s and both lose updates. This package does not retry that: polling stops at once, the buttons come off, and onWarn gets the reason with what to do about it (stop the other poller, or deleteWebhook). Give each process its own bot.

Large screenshots arrive as a file. Telegram will not take a photo over 10 MB, over 10 000 px of width plus height, or past a 20:1 aspect ratio. A screenshot past any of those is sent with sendDocument instead — no inline preview, but the caption, the buttons and the answer all work the same. A 1280x800 viewport is nowhere near any of these limits.

Security

  • The bot token is a credential and it is in every request URL. This package never logs it, and every error it throws is redacted before it is built — String(error) on a failed fetch carries the URL. Do not paste a token into an issue; if you have, revoke it with /revoke in @BotFather.
  • A button press is authorized twice. Telegram says a client may submit any callback_data it likes, so the data is a claim, not a credential. Each approval carries a fresh 96-bit random nonce, and the press has to come from the exact chat and message this package sent — which Telegram fills in itself and a stranger cannot forge. Knowing a handoff id, or seeing an old button, is not enough to answer anything.
  • Anyone in the chat can decide. A group chat means every member can press Approve, and Telegram's own tools decide who is in that group. Use a private chat with the bot, or a group whose membership is your approver list.
  • The takeover link is a bearer credential. Whoever opens it drives a live browser session carrying the agent's cookies. It dies with the handoff, and the message says as much — but a chat is a durable log, so post takeover links only where you would post a password.
  • The screenshot is the page. It can contain an account number, an address, a half-filled form. Telegram stores it; that is the trade for deciding from a phone. Approval mode sends one frame and no live view, which bounds what can ever leave.

Running the tests

bun run test        # everything, against a local fake Bot API server
bun run dist:smoke  # loads the built artifact under node
bun run test:live   # one real message, waits for a real tap

scripts/exit-probe.ts runs inside bun run test as a child process: it settles a handoff, then does nothing, and the suite asserts that it exits. It is not a unit test of anything — it is the only way to check the claim above from outside the process making it.

bun run test starts an actual HTTP server that answers like the Bot API — the multipart upload, the long poll that really blocks, the 401 for a wrong token — because a mock of Telegram would only prove the mock. test:live needs TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID and skips with instructions when they are missing.

Version note

This package needs handraise ≥ 0.5.1, the release that added channels. handraise is a peerDependency (>=0.5.1): nothing of it is imported at runtime, and a second nested copy would put two HandoffChannel declarations in one program. So install it yourself, alongside this one:

npm install handraise           # on npm
npm install handraise-telegram  # not yet — from this repo for now

License

MIT © Simon Doba