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

@agent-socket/sdk

v0.2.1

Published

Client SDK for agent-socket — connect any web app to AI chats via paste-able URLs.

Readme

@agent-socket/sdk

Client for Agent Socket. Your app registers tools and gets a link; an AI chat that has the link calls the tools over HTTPS, and the SDK runs your handlers.

Runs in browsers, Cloudflare Workers and Node 22+ on the built-in WebSocket. No dependencies.

npm i @agent-socket/sdk

In a page without a bundler: import { connect } from "https://esm.sh/@agent-socket/[email protected]".

Example

import { connect } from "@agent-socket/sdk"

let count = 0
const session = await connect({
  appId: "minimal-example",
  agentsMd: "# Counter\nA process with one counter. Call /increment to add to it.",
  tools: [{
    path: "/increment",
    description: "Add `by` (default 1) to the counter. Returns the new count.",
    input_schema: { type: "object", properties: { by: { type: "integer" } } },
    handler: ({ body }) => ({ count: (count += JSON.parse(body || "{}").by ?? 1) }),
  }],
})
const link = await session.mintAgentToken({ label: "user-42" })
console.log(link.url)  // https://agentsocket.dev/v1/t/as_.../agents.md

The AI fetches agents.md and tools.json from the link, then calls POST <link without /agents.md>/increment. Each call reaches your handler over the WebSocket, and the return value goes back as the HTTP response. More examples: examples/.

connect(options): Promise<Session>

Opens the WebSocket, registers, and resolves once the relay accepts. If this first attempt fails, the promise rejects and nothing retries.

| Option | | |---|---| | appId | Label shown in tools.json, [A-Za-z0-9_.-]{1,64}. Not a credential and not checked. | | agentsMd | Markdown briefing served at <link>/agents.md, up to 65,536 characters. Describe your app; the relay adds the calling instructions unless the text mentions tools.json. | | appDescription | Optional short description, shown in tools.json. | | tools | { method?, path, description, input_schema?, handler }[]. method defaults to POST. path is static, e.g. /set_pixel; /agents.md, /tools.json and /_as_* are reserved. | | baseUrl | Relay URL. Default https://agentsocket.dev. | | autoReconnect | Default true. See Reconnects. | | onDisconnect | ({ reason, code?, attempt, reconnect, giveUp }) => void. Called on a drop and after each failed attempt. code is the WebSocket close code when a socket closed. Default: exponentialBackoff(), or giveUp() when autoReconnect is false. | | onReconnect | ({ sessionId, resumed }) => void. Called after every successful reconnect. | | onSessionChanged | ({ priorSessionId, sessionId, tokensRemapped, reason, closeCode?, offlineMs }) => void. Called after a reconnect when links changed. tokensRemapped maps old URL to new URL; reason says why (see Reconnects). | | resume | { sessionId, secret } of an earlier session, to keep its links after a restart. See Surviving a restart. | | heartbeatIntervalMs | Ping after this long without traffic. Default 25000. The ping is the fixed frame {"type":"ping","id":"as_hb"}, which the relay's runtime answers without waking the session. | | heartbeatTimeoutMs | Close and reconnect if no pong arrives within this. Default 50000. |

Handlers

A handler gets { method, path, body, headers }. body is the raw request body as a string (usually JSON). headers has the content-type and the agent's x-* headers.

Return either:

  • any value: sent as JSON with status 200, or
  • { status, body?, headers?, taskId? }: any object with a numeric status is read this way. If headers sets content-type and body is a string, the body is sent as-is with that type (HTML and XML types are served as text/plain). Other headers are ignored.

A handler that throws produces 500 {"error": {"code": "handler_error", "message": ...}}.

Session

| Member | | |---|---| | sessionId | Current session id. Changes only when a reconnect can't resume. | | connected | true while the socket is open and registered. | | resumeSecret | Secret for resuming this session. Treat it like the links. | | mintAgentToken({ label }) | New link: { token, url, label, expiresAt: null }. Up to 50 per session (rejects with mint failed: too_many_tokens). | | listAgentTokens() | Active links, with mintedAt. Needs a live connection. | | revokeAgentToken(token) | Kills a link. While disconnected, the revoke is sent with the next resume. | | updateTools(tools, agentsMd?) | Replaces the tool list (and agentsMd if given) on the live session; every link keeps working. Rejects if the relay refuses the list, and then nothing changes. Calls run in order; while disconnected it waits for the reconnect. | | completeTask(taskId, { status?, body?, headers? }) | Finishes an async call. Throws if not connected. | | ping() | Sends a heartbeat now, e.g. from a chrome.alarms handler in an MV3 service worker. | | close() | Closes with code 1000. The relay ends the session at once and every link stops working. Called while disconnected, it sends the relay an end for the held session instead (best effort; if the relay can't be reached, the session ends when its hold runs out). |

Async tools

The relay waits up to 30 s for a reply. For longer work, return 202 with a task id and finish later:

{
  path: "/render_report",
  description: "Start a report. Poll the returned task.",
  handler: ({ body }) => {
    const taskId = crypto.randomUUID()
    renderReport(body).then((result) => session.completeTask(taskId, { body: result }))
    return { status: 202, taskId }
  },
}

The agent gets 202 {"taskId": "..."} and polls <link base>/_as_tasks/<taskId> until it gets the result. Task ids must match [A-Za-z0-9_-]{1,64}. Tasks survive a resumed reconnect but not a new session.

Reconnects

When the socket drops, the relay keeps the session for 24 hours (RESUME_GRACE_MS on a self-hosted relay), in Durable Object storage, so it also survives the relay restarting. With autoReconnect on (the default), the SDK backs off via onDisconnect, reconnects and resumes the same session with its secret. The default backoff never gives up and waits at most 30 s (±25%) between attempts, so an app coming back after hours offline (a laptop waking up) resumes within about 30 s of the network returning. On success every link keeps working, onReconnect gets resumed: true, and onSessionChanged is not called. While the app is away, agents still get agents.md and tools.json, and tool calls get 503 app_offline with Retry-After: 2.

If the relay refuses the resume (the hold ran out, or the relay lost the session), the SDK opens a new session in the same attempt and re-mints each link it still holds with the same label. The old URLs stop working; onSessionChanged reports the new ones so you can show them to the user. Tell the user plainly that the link changed: an AI still holding the old one only gets 503 app_offline. Links revoked while offline are not re-minted.

onSessionChanged's reason is one of:

| reason | closeCode | Meaning | |---|---|---| | resume_refused | 4401 | The relay no longer had the session: the app was away longer than the hold (24 h on agentsocket.dev), or the session was ended or lost on the relay. (A wrong secret gets the same answer.) | | replaced | 4410 | Another connection resumed the session with its secret, so the SDK started a fresh one instead of taking it back. | | no_resume_secret | | The relay never issued a resume secret, so there was nothing to resume. | | remint | | Same session; links an earlier, interrupted re-mint missed were minted now. |

offlineMs is the time from the last frame the relay sent on the old connection to the new registration, roughly how long the app was unreachable (it can overstate a quiet connection by up to one heartbeat interval). Longer than the relay's hold means the outage outlasted it; much less means the relay lost or ended the session.

With autoReconnect: false the SDK neither reconnects nor re-mints. Your onDisconnect can still call reconnect(), which resumes when possible; if it lands in a new session, the old links are gone and tokensRemapped is empty.

Backoff helpers for onDisconnect: exponentialBackoff({ baseMs = 1000, maxMs = 30000, jitter = 0.25 }), linearBackoff({ delayMs = 5000 }), noBackoff(). None of them gives up; a custom onDisconnect that calls giveUp() stops reconnecting for good. Keep the interval well under the relay's hold.

Surviving a restart

To keep links across a page reload or a service-worker restart, save { sessionId: session.sessionId, secret: session.resumeSecret } and pass it as resume to the next connect(). If the session is still held, you get the same sessionId and its links keep working. If not, connect() opens a new session; compare sessionId to tell, and mint new links. Store the secret no more widely than the links themselves (sessionStorage or chrome.storage.session, not localStorage). If the relay can't be reached yet, connect() rejects: keep what you saved and try again later rather than throwing it away, since the relay keeps the session for its hold.

endSession({ baseUrl, sessionId, secret }) ends a saved session without reconnecting to it (e.g. the user pressed Stop while offline): the links stop at once. It resolves true when the relay confirmed, false when it refused or couldn't be reached within 10 s.

defaultAgentsMd(options)

Builds a briefing with the calling instructions, a "save this URL" line and sections for what the AI can and cannot do: { appName, appDescription, agentsMdUrl, capabilities?, limitations?, conventions? }. agentsMdUrl is shown to the AI as the URL to re-fetch; "$BASE/agents.md" works.

Security

Anyone with a link can call your tools until you revoke it or the session ends. Only register tools you are willing to give to whoever ends up with the link. See SECURITY.md and the protocol.

License

Apache 2.0