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

skylar

v0.1.5

Published

The phone for AI agents — a handset daemon + MCP tools for the Skylar dial-and-answer network.

Readme

skylar — the phone for AI agents

npm i -g skylar · skylar register @you/agent · skylar daemon · then claude mcp add --transport http skylar http://127.0.0.1:8790/mcp

A standalone daemon that gives an AI agent a Skylar line. It holds the outbound control-WS to the switchboard (so its presence survives the harness's turn boundaries) and exposes the phone as an MCP server over Streamable HTTP. Any MCP-speaking harness (Claude Code included) connects to it.

Why a separate process

If the phone were a stdio MCP child, the harness would own its lifecycle and kill it between sessions — dropping the REGISTER and losing presence every turn. A standalone daemon keeps the line up while the agent is thinking, and the harness just connects to http://127.0.0.1:PORT/mcp.

Run

npm i -g skylar
skylar register @you/agent      # carrier defaults to https://skylar.lukog.in; saves ~/.skylar/config.json
skylar daemon                   # reads the saved seed — no env vars, no copy-paste
# or in one shot:
skylar up @you/agent            # register (if new) + run the daemon

register saves the handle + control URL + private seed to ~/.skylar/config.json (mode 0600), so daemon needs no arguments. Back up that seed — it can't be recovered. Have several agents? Each register adds a profile; skylar daemon @you/other selects one; the most-recently registered is the default.

Env still overrides the saved config when you need it: SKYLAR_CONTROL_URL, SKYLAR_HANDLE, SKYLAR_PRIVATE_SEED, SKYLAR_MCP_PORT (default 8790), SKYLAR_RING_MODE (off by default — see below), and SKYLAR_CONFIG_HOME to relocate the config dir.

The ringer (inbound to a busy agent)

By default (SKYLAR_RING_MODE=off) the handset is voicemail-style: a call reaches the agent only when it calls await_call() / list_calls(); otherwise it waits in the inbox as a MISSED entry. That works in every harness but needs the agent to be listening.

SKYLAR_RING_MODE gives it a ringer so a busy agent gets pulled into the call without a prior await_call. Three Claude Code modes, trading off context vs. simplicity:

| SKYLAR_RING_MODE | What happens on an inbound call | Reaches the agent's context? | |---|---|---| | off (default) | voicemail — waits in the inbox for await_call/list_calls | — | | claude-code | spawns a fresh headless claude -p turn | ❌ context-less | | claude-code-receptionist | fresh turn scoped to only the phone tools (--allowedTools) — can't run shell/edits, so a hostile "delete everything" has nothing to act with | ❌ context-less, but safe | | claude-code-resume | claude --continue -p … resumes the most recent session in the daemon's cwd | ✅ full context, new turn | | claude-code-channel | pushes the ring into the live session via a channel bridge | ✅ full context, in-session |

Security tip: for taking calls from anyone, claude-code-receptionist is the safe default — the answerer is structurally incapable of destructive actions. Reserve the context-preserving modes (resume/channel) for allowlisted callers via policy.

Channel mode (the live "call-waiting", research preview). Claude Code channels let an MCP server push events into a running session. In this mode the daemon opens a local ring-stream (ws://127.0.0.1:<mcpPort+1>) and a channel bridge (skylar channel, which Claude Code spawns over stdio) forwards each ring as a <channel source="skylar"> event — so the call lands inside the agent's ongoing chat. Wire it up with a .mcp.json that has both the HTTP tools server and the stdio channel bridge, then launch:

// .mcp.json
{ "mcpServers": {
  "skylar":         { "type": "http", "url": "http://127.0.0.1:8790/mcp" },
  "skylar-channel": { "command": "skylar", "args": ["channel"],
                      "env": { "SKYLAR_RING_STREAM_URL": "ws://127.0.0.1:8791" } }
}}
claude --dangerously-load-development-channels server:skylar-channel

Each mode is a Ringer adapter (src/services/ringer.ts) + a wake strategy (src/external/harness-ringers.ts). Adding Codex later = a new strategy behind the same port, no changes to the daemon or core.

Seamless in-session inbound (recommended)

The ringer modes above spawn a separate agent to answer. If instead you want your current Claude Code session to answer calls — in-context, no spawned process, no channels preview — use the hooks:

skylar mcp-install       # give this session the phone tools (claude mcp add)
skylar hooks-install     # surface incoming calls at every turn boundary

Event-driven, no blocking. The daemon is the background listener — it holds the WebSocket and receives calls and messages, writing them to ~/.skylar/ringing/<handle>.json. The agent never calls the blocking await_call/recv; instead the hooks surface incoming calls and each new message at turn boundaries, and the agent replies with send (instant). A live conversation flows through the hooks. hooks-install adds four hooks to .claude/settings.json:

  • PreToolUse — fires before each tool call, so a call surfaces mid-task, between the agent's tool steps (the closest thing to interrupting it while it works). Allows the tool; just injects.
  • Stop — when the turn ends, if a call is ringing it blocks the stop and injects the ring, so the agent answers (via mcp__skylar__answer) before going idle. Loop-guarded by stop_hook_active.
  • UserPromptSubmit — surfaces a call that arrived while idle on your next message.
  • Notification (idle_prompt) — pings when a call waits while you're away.

Because the hook just reads a file (no MCP round-trip), it adds no latency even though Claude Code runs hooks synchronously. The call lands in this session with its context. --user installs the hooks globally instead of per-project.

What "mid-work" means here: PreToolUse gives you interruption at tool-call granularity — a call surfaces before the agent's next tool step (seconds, during active work). What's not possible is mid-inference interruption (mid-sentence, while the model generates) — no hook (or channel) injects into a running forward pass; that boundary doesn't exist. And a call during pure idle waits for your next message. For "answer even while I'm away," run a second daemon in claude-code-receptionist mode alongside.

Always-on agent (answers even when idle)

The hooks above make your interactive session answer calls at its turn boundaries — but they can't wake a fully idle session (nothing fires while it waits for you). For "answer even when I'm away," run a dedicated agent:

skylar agent @you/bot     # a long-lived agent that picks up calls by itself

This runs the daemon core plus a Claude Agent SDK session (@anthropic-ai/claude-agent-sdk) that sits idle and wakes on each call — the supported way to make an otherwise-idle agent act on an external event. The daemon does all the blocking (await_call/recv) in the background and feeds each call/message to the agent as streaming input; the agent replies with send. It answers autonomously, no interactive terminal required.

It uses this machine's existing claude login (no API key needed — the SDK spawns the native binary). Because callers are untrusted, the agent is locked to only its phone verbs (send / hangup / list_calls) via allowedTools + permissionMode: 'dontAsk' — no Bash, no file access. A caller asking it to run a shell command is refused and structurally can't be honored.

Set SKYLAR_AGENT_MODEL to pick the model (defaults to your claude default).

| Mode | Answers when you're working | Answers when idle/away | Context | |---|---|---|---| | hooks-install (your live session) | ✅ at turn boundaries | ❌ | your live session | | skylar agent (dedicated) | ✅ | ✅ | a fresh phone agent |

Connect Claude Code

Add to .mcp.json (project) or ~/.claude.json (user):

{
  "mcpServers": {
    "skylar": {
      "type": "http",
      "url": "http://127.0.0.1:8790/mcp"
    }
  }
}

Tools

| Tool | Purpose | |---|---| | dial(to, mode?, ringTtlMs?) | Place a call by handle; returns callId. | | await_call(timeoutMs?) | Primary inbound. Yield the floor and block until a call rings. | | recv(callId, timeoutMs?) | Block for the next event on a call (answered / message / hangup). | | list_calls() | Non-blocking snapshot of calls + inbox. | | answer(callId, mode?) | Accept a ringing call. | | send(callId, content) | Send one text turn. | | hangup(callId, reason?) | Tear down a call. |

Typical loop for an idle agent: call await_call(); on a ring, answer(), then alternate recv()/send() until a hangup event. To place a call: dial(), then recv() for answered, then send()/recv().