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

@rocksky/sdk

v0.16.0

Published

TypeScript SDK for Rocksky — built on atcute: AppView reads, AT Protocol PDS writes (scrobble, like, follow, shout), a local dedup index, and Jetstream real-time sync.

Downloads

2,397

Readme

@rocksky/sdk

Official TypeScript SDK for Rocksky — a music scrobbling & discovery platform on the AT Protocol. Built on atcute: RockskyClient does unauthenticated AppView reads, and Agent logs in with an app password and writes app.rocksky.* records to the user's PDS.

Install

npm install @rocksky/sdk      # or: bun add @rocksky/sdk

Quickstart

import { RockskyClient, Agent } from "@rocksky/sdk";

// Reads — unauthenticated. new RockskyClient() uses https://api.rocksky.app.
const rk = new RockskyClient();
const stats = await rk.globalStats();
const top = await rk.topTracks(10, 0);

// Writes — log in with an app password (resolves the PDS automatically).
const agent = await Agent.login("alice.bsky.social", "app-password");
const uri = await agent.scrobble({
  title: "Chaser", artist: "Calibro 35",
  album: "Jazzploitation", albumArtist: "Calibro 35", duration: 182320,
});

API

Reads — RockskyClient: the client now covers the whole app.rocksky.* read surface. Typed methods include profile, scrobbles, songs, albums, artists, topTracks, topArtists, search, globalStats, lovedSongs, catalogAlbums, catalogArtists, catalogSongs, albumTracks, artistAlbums, artistTracks, scrobbleFeed, scrobble (single by uri), follows, followers, knownFollowers. Raw (unknown-returning) detail/long-tail methods cover the rest: album, artist, song, feed, playlists, playlist, stats, wrapped, scrobblesChart, recommendations, neighbours, shouts, and more.

Every named method is sugar over the universal escape hatch rk.get(nsid, params), which calls ANY read query by nsid and returns unknown.

Filtering: catalogSongs, catalogArtists, catalogAlbums, and scrobbleFeed take an optional RSQL filter (string or Filter) — build one fluently, with the per-endpoint field constants for discoverability:

import { Filter, RockskyClient, ScrobbleFields, SongFields } from "@rocksky/sdk";

const rk = new RockskyClient();
const filter = Filter.eq(SongFields.artist, "Daft Punk")
  .and(Filter.gt(SongFields.duration, 200_000))
  .or(Filter.in(SongFields.genre, ["house", "electro"]));
const songs = await rk.catalogSongs(50, 0, undefined, filter);
// filter sent: artist=="Daft Punk";duration=gt=200000,genre=in=(house,electro)

// scrobbles support dotted selectors into the joined track/user/artist:
await rk.scrobbleFeed(undefined, false, 50, 0,
  Filter.eq(ScrobbleFields.trackArtist, "Daft Punk").and(Filter.ge(ScrobbleFields.date, "2025-01-01")));

Operators: eq (supports * wildcards), ne, gt, ge, lt, le, in, out, isNull, isNotNull, chained with .and() / .or() (parentheses inserted automatically per RSQL precedence).

Typed date-window charts: topTracksInterval(limit, offset, interval) and topArtistsInterval(...) take a DateInterval built with the Interval factories — Interval.allTime(), Interval.lastDays(n), Interval.lastWeeks(n), Interval.lastMonths(n), Interval.lastYears(n), Interval.range(start, end). topTracks / topArtists remain all-time shorthands.

import { RockskyClient, Interval } from "@rocksky/sdk";

const rk = new RockskyClient();
const monthly = await rk.topTracksInterval(10, 0, Interval.lastMonths(1));

// Resolve a bare title + artist into full canonical metadata.
const song = await rk.matchSong("Chaser", "Calibro 35");

matchSong(title, artist, mbId?, isrc?): resolves a bare title + artist into full canonical metadata (album, artwork, duration, MBID, ISRC, links).

Auth-gated reads: pass an optional bearer access token — new RockskyClient(appview, token) — and it is sent as Authorization: Bearer <token>.

Writes — Agent: two scrobble paths. scrobble(rec) writes full metadata you already have; scrobbleMatch(input, appview?) takes a ScrobbleMatchInput object ({ title, artist, album?, mbId?, isrc?, timestamp? } — title/artist required; album overrides the resolved album, mbId/isrc are match anchors, timestamp is a scrobbled-at Unix-seconds time; the optional appview overrides the AppView used for matching) and resolves full metadata via matchSong first, then writes — e.g. agent.scrobbleMatch({ title: "Chaser", artist: "Calibro 35" }). Plus createSong/createAlbum/createArtist, like, follow, shout/replyShout, setNowPlaying/clearNowPlaying, delete. Records are the generated types from ./generated/types.

Identity hashes: songHash, albumHash, artistHash — lowercase-hex SHA-256, identical to the server and every other Rocksky SDK.

Duplicate prevention + real-time sync

An optional local index (embedded classic-level LevelDB) prevents duplicate writes and stays live off the firehose. It is Node-only, so it lives on its own subpath — the main @rocksky/sdk entry stays browser-safe:

import { RockskyIndex } from "@rocksky/sdk/dedup";

const idx = new RockskyIndex("./dedup");
await idx.open();
agent.useIndex(idx);

const stats = await agent.syncRepo();     // backfill from the repo CAR (com.atproto.sync.getRepo)
agent.hydrateFromJetstream();             // keep it live from Jetstream (all 4 servers)

With an index attached, the write verbs skip records that already exist (return the existing URI) and a same-second scrobble of the same track isn't duplicated.

Node ≥ 22 (global WebSocket/fetch) or Bun.

Remote control

Build a controllable player or a remote UI over the remote-control WebSocket (see remote-ws/PROTOCOL.md). Heartbeat, reconnect, and the register handshake are handled for you.

import { RemotePlayer, RemoteController } from "@rocksky/sdk";

// A controllable player (advertises now-playing + obeys commands):
const player = new RemotePlayer({ token, name: "My Player" });
player.on("play", () => engine.play()).on("pause", () => engine.pause());
player.connect();
player.setNowPlaying({ title, artist, albumArt, durationMs, elapsedMs, isPlaying: true });
player.setStatus("playing");

// A controller (lists devices, picks the primary, sends commands):
const controller = new RemoteController({ token, name: "My Controller" });
controller.on("devices", ({ devices }) => render(devices));
controller.on("nowPlaying", ({ deviceId, track }) => show(deviceId, track));
controller.connect();
controller.setPrimary(deviceId);
controller.pause(deviceId);

See examples/remote.ts.

Example

bun run examples/native.ts

License

MIT.