A decentralized music tracking and discovery platform built on AT Protocol 🎵
listenbrainz spotify atproto lastfm musicbrainz scrobbling

Add JetStream sync CLI and client

Introduce a JetStreamClient implementation and a new sync command
(apps/cli/src/cmd/sync.ts), add structured logging via @logtape/logtape,
and wire a logger into the CLI context. Remove the old @atcute/jetstream
dependency and add @logtape/logtape. Simplify scrobble CLI to a stub and
adjust schema imports to .js. Fix the now-playing query to select the
latest scrobble per user using DISTINCT ON.

+6 -6
apps/api/src/xrpc/app/rocksky/feed/getNowPlayings.ts
··· 21 21 Effect.map((data) => ({ data })), 22 22 Effect.flatMap(presentation), 23 23 Effect.retry({ times: 3 }), 24 - Effect.timeout("120 seconds") 24 + Effect.timeout("120 seconds"), 25 25 ), 26 26 }); 27 27 ··· 32 32 Effect.catchAll((err) => { 33 33 console.error(err); 34 34 return Effect.succeed({}); 35 - }) 35 + }), 36 36 ); 37 37 server.app.rocksky.feed.getNowPlayings({ 38 38 handler: async ({ params }) => { ··· 78 78 .leftJoin(tracks, eq(scrobbles.trackId, tracks.id)) 79 79 .leftJoin(users, eq(scrobbles.userId, users.id)) 80 80 .where( 81 - sql`scrobbles.timestamp = ( 82 - SELECT MAX(inner_s.timestamp) 81 + sql`scrobbles.id IN ( 82 + SELECT DISTINCT ON (inner_s.user_id) inner_s.id 83 83 FROM scrobbles inner_s 84 - WHERE inner_s.user_id = ${users.id} 85 - )` 84 + ORDER BY inner_s.user_id, inner_s.timestamp DESC, inner_s.id DESC 85 + )`, 86 86 ) 87 87 .orderBy(desc(scrobbles.timestamp)) 88 88 .limit(params.size || 20)
+1 -1
apps/cli/package.json
··· 23 23 "author": "Tsiry Sandratraina <tsiry.sndr@rocksky.app>", 24 24 "license": "Apache-2.0", 25 25 "dependencies": { 26 - "@atcute/jetstream": "^1.1.2", 27 26 "@atproto/api": "^0.13.31", 28 27 "@atproto/common": "^0.4.6", 29 28 "@atproto/identity": "^0.4.5", ··· 32 31 "@atproto/lexicon": "^0.4.5", 33 32 "@atproto/sync": "^0.1.11", 34 33 "@atproto/syntax": "^0.3.1", 34 + "@logtape/logtape": "^1.3.6", 35 35 "@modelcontextprotocol/sdk": "^1.10.2", 36 36 "@paralleldrive/cuid2": "^3.0.6", 37 37 "@types/better-sqlite3": "^7.6.13",
+1 -67
apps/cli/src/cmd/scrobble.ts
··· 1 - import chalk from "chalk"; 2 - import { RockskyClient } from "client"; 3 - import fs from "fs/promises"; 4 - import md5 from "md5"; 5 - import os from "os"; 6 - import path from "path"; 7 - 8 1 export async function scrobble(track, artist, { timestamp }) { 9 - const tokenPath = path.join(os.homedir(), ".rocksky", "token.json"); 10 - try { 11 - await fs.access(tokenPath); 12 - } catch (err) { 13 - console.error( 14 - `You are not logged in. Please run ${chalk.greenBright( 15 - "`rocksky login <username>.bsky.social`" 16 - )} first.` 17 - ); 18 - return; 19 - } 20 - 21 - const tokenData = await fs.readFile(tokenPath, "utf-8"); 22 - const { token } = JSON.parse(tokenData); 23 - if (!token) { 24 - console.error( 25 - `You are not logged in. Please run ${chalk.greenBright( 26 - "`rocksky login <username>.bsky.social`" 27 - )} first.` 28 - ); 29 - return; 30 - } 31 - 32 - const client = new RockskyClient(token); 33 - const apikeys = await client.getApiKeys(); 34 - 35 - if (!apikeys || apikeys.length === 0 || !apikeys[0].enabled) { 36 - console.error( 37 - `You don't have any API keys. Please create one using ${chalk.greenBright( 38 - "`rocksky create apikey`" 39 - )} command.` 40 - ); 41 - return; 42 - } 43 - 44 - const signature = md5( 45 - `api_key${ 46 - apikeys[0].apiKey 47 - }artist[0]${artist}methodtrack.scrobblesk${token}timestamp[0]${ 48 - timestamp || Math.floor(Date.now() / 1000) 49 - }track[0]${track}${apikeys[0].sharedSecret}` 50 - ); 51 - 52 - const response = await client.scrobble( 53 - apikeys[0].apiKey, 54 - signature, 55 - track, 56 - artist, 57 - timestamp 58 - ); 59 - 60 - console.log( 61 - `Scrobbled ${chalk.greenBright(track)} by ${chalk.greenBright( 62 - artist 63 - )} at ${chalk.greenBright( 64 - new Date( 65 - (timestamp || Math.floor(Date.now() / 1000)) * 1000 66 - ).toLocaleString() 67 - )}` 68 - ); 2 + console.log(">> scrobble", track, artist, timestamp); 69 3 }
+75
apps/cli/src/cmd/sync.ts
··· 1 + import { JetStreamClient, JetStreamEvent } from "jetstream"; 2 + import chalk from "chalk"; 3 + import { logger } from "logger"; 4 + 5 + const getEndpoint = () => { 6 + const endpoint = process.env.JETSTREAM_SERVER 7 + ? process.env.JETSTREAM_SERVER 8 + : "wss://jetstream1.us-west.bsky.network/subscribe"; 9 + 10 + if (endpoint?.endsWith("/subscribe")) { 11 + return endpoint; 12 + } 13 + 14 + return `${endpoint}/subscribe`; 15 + }; 16 + 17 + export function sync() { 18 + const client = new JetStreamClient({ 19 + wantedCollections: [ 20 + "app.rocksky.scrobble", 21 + "app.rocksky.artist", 22 + "app.rocksky.album", 23 + "app.rocksky.song", 24 + ], 25 + endpoint: getEndpoint(), 26 + 27 + // Optional: filter by specific DIDs 28 + // wantedDids: ["did:plc:example123"], 29 + 30 + // Reconnection settings 31 + maxReconnectAttempts: 10, 32 + reconnectDelay: 1000, 33 + maxReconnectDelay: 30000, 34 + backoffMultiplier: 1.5, 35 + 36 + // Enable debug logging 37 + debug: true, 38 + }); 39 + 40 + client.on("open", () => { 41 + logger.info`✅ Connected to JetStream!`; 42 + }); 43 + 44 + client.on("message", async (data) => { 45 + const event = data as JetStreamEvent; 46 + 47 + if (event.kind === "commit" && event.commit) { 48 + const { operation, collection, record, rkey } = event.commit; 49 + const uri = `at://${event.did}/${collection}/${rkey}`; 50 + 51 + logger.info`\n📡 New event:`; 52 + logger.info` Operation: ${operation}`; 53 + logger.info` Collection: ${collection}`; 54 + logger.info` DID: ${event.did}`; 55 + logger.info` Uri: ${uri}`; 56 + 57 + if (operation === "create" && record) { 58 + console.log(JSON.stringify(record, null, 2)); 59 + } 60 + 61 + logger.info` Cursor: ${event.time_us}`; 62 + } 63 + }); 64 + 65 + client.on("error", (error) => { 66 + logger.error`❌ Error: ${error}`; 67 + }); 68 + 69 + client.on("reconnect", (data) => { 70 + const { attempt } = data as { attempt: number }; 71 + logger.info`🔄 Reconnecting... (attempt ${attempt})`; 72 + }); 73 + 74 + client.connect(); 75 + }
+5 -1
apps/cli/src/context.ts
··· 1 + import { logger } from "logger"; 1 2 import drizzle from "./drizzle"; 2 3 3 - export const context = { 4 + export const ctx = { 4 5 db: drizzle.db, 6 + logger, 5 7 }; 8 + 9 + export type Context = typeof ctx;
+10 -4
apps/cli/src/index.ts
··· 15 15 import { Command } from "commander"; 16 16 import version from "../package.json" assert { type: "json" }; 17 17 import { login } from "./cmd/login"; 18 + import { sync } from "cmd/sync"; 18 19 19 20 const program = new Command(); 20 21 ··· 22 23 .name("rocksky") 23 24 .description( 24 25 `Command-line interface for Rocksky (${chalk.underline( 25 - "https://rocksky.app" 26 - )}) – scrobble tracks, view stats, and manage your listening history.` 26 + "https://rocksky.app", 27 + )}) – scrobble tracks, view stats, and manage your listening history.`, 27 28 ) 28 29 .version(version.version); 29 30 ··· 42 43 .command("nowplaying") 43 44 .argument( 44 45 "[did]", 45 - "the DID or handle of the user to get the now playing track for." 46 + "the DID or handle of the user to get the now playing track for.", 46 47 ) 47 48 .description("get the currently playing track.") 48 49 .action(nowplaying); ··· 63 64 .option("-l, --limit <number>", "number of results to limit") 64 65 .argument( 65 66 "<query>", 66 - "the search query, e.g., artist, album, title or account" 67 + "the search query, e.g., artist, album, title or account", 67 68 ) 68 69 .description("search for tracks, albums, or accounts.") 69 70 .action(search); ··· 117 118 .command("mcp") 118 119 .description("Starts an MCP server to use with Claude or other LLMs.") 119 120 .action(mcp); 121 + 122 + program 123 + .command("sync") 124 + .description("Sync your local Rocksky data from AT Protocol.") 125 + .action(sync); 120 126 121 127 program.parse(process.argv);
+285
apps/cli/src/jetstream.ts
··· 1 + export interface JetStreamEvent { 2 + did: string; 3 + time_us: number; 4 + kind: "commit" | "identity" | "account"; 5 + commit?: { 6 + rev: string; 7 + operation: "create" | "update" | "delete"; 8 + collection: string; 9 + rkey: string; 10 + record?: Record<string, unknown>; 11 + cid?: string; 12 + }; 13 + identity?: { 14 + did: string; 15 + handle?: string; 16 + seq?: number; 17 + time?: string; 18 + }; 19 + account?: { 20 + active: boolean; 21 + did: string; 22 + seq: number; 23 + time: string; 24 + }; 25 + } 26 + 27 + export interface JetStreamClientOptions { 28 + endpoint?: string; 29 + wantedCollections?: string[]; 30 + wantedDids?: string[]; 31 + maxReconnectAttempts?: number; 32 + reconnectDelay?: number; 33 + maxReconnectDelay?: number; 34 + backoffMultiplier?: number; 35 + debug?: boolean; 36 + } 37 + 38 + export type JetStreamEventType = 39 + | "open" 40 + | "message" 41 + | "error" 42 + | "close" 43 + | "reconnect"; 44 + 45 + export class JetStreamClient { 46 + private ws: WebSocket | null = null; 47 + private options: Required<JetStreamClientOptions>; 48 + private reconnectAttempts = 0; 49 + private reconnectTimer: number | null = null; 50 + private isManualClose = false; 51 + private eventHandlers: Map< 52 + JetStreamEventType, 53 + Set<(data?: unknown) => void> 54 + > = new Map(); 55 + private cursor: number | null = null; 56 + 57 + constructor(options: JetStreamClientOptions = {}) { 58 + this.options = { 59 + endpoint: 60 + options.endpoint || "wss://jetstream1.us-east.bsky.network/subscribe", 61 + wantedCollections: options.wantedCollections || [], 62 + wantedDids: options.wantedDids || [], 63 + maxReconnectAttempts: options.maxReconnectAttempts ?? Infinity, 64 + reconnectDelay: options.reconnectDelay ?? 1000, 65 + maxReconnectDelay: options.maxReconnectDelay ?? 30000, 66 + backoffMultiplier: options.backoffMultiplier ?? 1.5, 67 + debug: options.debug ?? false, 68 + }; 69 + 70 + // Initialize event handler sets 71 + ["open", "message", "error", "close", "reconnect"].forEach((event) => { 72 + this.eventHandlers.set(event as JetStreamEventType, new Set()); 73 + }); 74 + } 75 + 76 + /** 77 + * Register an event handler 78 + */ 79 + on(event: JetStreamEventType, handler: (data?: unknown) => void): this { 80 + this.eventHandlers.get(event)?.add(handler); 81 + return this; 82 + } 83 + 84 + /** 85 + * Remove an event handler 86 + */ 87 + off(event: JetStreamEventType, handler: (data?: unknown) => void): this { 88 + this.eventHandlers.get(event)?.delete(handler); 89 + return this; 90 + } 91 + 92 + /** 93 + * Emit an event to all registered handlers 94 + */ 95 + private emit(event: JetStreamEventType, data?: unknown): void { 96 + this.eventHandlers.get(event)?.forEach((handler) => { 97 + try { 98 + handler(data); 99 + } catch (error) { 100 + this.log("error", `Handler error for ${event}:`, error); 101 + } 102 + }); 103 + } 104 + 105 + /** 106 + * Build the WebSocket URL with query parameters 107 + */ 108 + private buildUrl(): string { 109 + const url = new URL(this.options.endpoint); 110 + 111 + if (this.options.wantedCollections.length > 0) { 112 + this.options.wantedCollections.forEach((collection) => { 113 + url.searchParams.append("wantedCollections", collection); 114 + }); 115 + } 116 + 117 + if (this.options.wantedDids.length > 0) { 118 + this.options.wantedDids.forEach((did) => { 119 + url.searchParams.append("wantedDids", did); 120 + }); 121 + } 122 + 123 + if (this.cursor !== null) { 124 + url.searchParams.set("cursor", this.cursor.toString()); 125 + } 126 + 127 + return url.toString(); 128 + } 129 + 130 + /** 131 + * Connect to the JetStream WebSocket 132 + */ 133 + connect(): void { 134 + if (this.ws && this.ws.readyState === WebSocket.OPEN) { 135 + this.log("warn", "Already connected"); 136 + return; 137 + } 138 + 139 + this.isManualClose = false; 140 + const url = this.buildUrl(); 141 + this.log("info", `Connecting to ${url}`); 142 + 143 + try { 144 + this.ws = new WebSocket(url); 145 + 146 + this.ws.onopen = () => { 147 + this.log("info", "Connected successfully"); 148 + this.reconnectAttempts = 0; 149 + this.emit("open"); 150 + }; 151 + 152 + this.ws.onmessage = (event) => { 153 + try { 154 + const data = JSON.parse(event.data) as JetStreamEvent; 155 + 156 + // Update cursor for resumption 157 + if (data.time_us) { 158 + this.cursor = data.time_us; 159 + } 160 + 161 + this.emit("message", data); 162 + } catch (error) { 163 + this.log("error", "Failed to parse message:", error); 164 + this.emit("error", { type: "parse_error", error }); 165 + } 166 + }; 167 + 168 + this.ws.onerror = (event) => { 169 + this.log("error", "WebSocket error:", event); 170 + this.emit("error", event); 171 + }; 172 + 173 + this.ws.onclose = (event) => { 174 + this.log("info", `Connection closed: ${event.code} ${event.reason}`); 175 + this.emit("close", event); 176 + 177 + if (!this.isManualClose) { 178 + this.scheduleReconnect(); 179 + } 180 + }; 181 + } catch (error) { 182 + this.log("error", "Failed to create WebSocket:", error); 183 + this.emit("error", { type: "connection_error", error }); 184 + this.scheduleReconnect(); 185 + } 186 + } 187 + 188 + /** 189 + * Schedule a reconnection attempt with exponential backoff 190 + */ 191 + private scheduleReconnect(): void { 192 + if (this.reconnectAttempts >= this.options.maxReconnectAttempts) { 193 + this.log("error", "Max reconnection attempts reached"); 194 + return; 195 + } 196 + 197 + const delay = Math.min( 198 + this.options.reconnectDelay * 199 + Math.pow(this.options.backoffMultiplier, this.reconnectAttempts), 200 + this.options.maxReconnectDelay, 201 + ); 202 + 203 + this.reconnectAttempts++; 204 + this.log( 205 + "info", 206 + `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`, 207 + ); 208 + 209 + this.reconnectTimer = setTimeout(() => { 210 + this.emit("reconnect", { attempt: this.reconnectAttempts }); 211 + this.connect(); 212 + }, delay) as unknown as number; 213 + } 214 + 215 + /** 216 + * Manually disconnect from the WebSocket 217 + */ 218 + disconnect(): void { 219 + this.isManualClose = true; 220 + 221 + if (this.reconnectTimer !== null) { 222 + clearTimeout(this.reconnectTimer); 223 + this.reconnectTimer = null; 224 + } 225 + 226 + if (this.ws) { 227 + this.ws.close(); 228 + this.ws = null; 229 + } 230 + 231 + this.log("info", "Disconnected"); 232 + } 233 + 234 + /** 235 + * Update subscription filters (requires reconnection) 236 + */ 237 + updateFilters(options: { 238 + wantedCollections?: string[]; 239 + wantedDids?: string[]; 240 + }): void { 241 + if (options.wantedCollections) { 242 + this.options.wantedCollections = options.wantedCollections; 243 + } 244 + if (options.wantedDids) { 245 + this.options.wantedDids = options.wantedDids; 246 + } 247 + 248 + // Reconnect with new filters 249 + if (this.ws) { 250 + this.disconnect(); 251 + this.connect(); 252 + } 253 + } 254 + 255 + /** 256 + * Get current connection state 257 + */ 258 + get readyState(): number { 259 + return this.ws?.readyState ?? WebSocket.CLOSED; 260 + } 261 + 262 + /** 263 + * Check if currently connected 264 + */ 265 + get isConnected(): boolean { 266 + return this.ws?.readyState === WebSocket.OPEN; 267 + } 268 + 269 + /** 270 + * Get current cursor position 271 + */ 272 + get currentCursor(): number | null { 273 + return this.cursor; 274 + } 275 + 276 + /** 277 + * Logging utility 278 + */ 279 + private log(level: "info" | "warn" | "error", ...args: unknown[]): void { 280 + if (this.options.debug || level === "error") { 281 + const prefix = `[JetStream ${level.toUpperCase()}]`; 282 + console[level](prefix, ...args); 283 + } 284 + } 285 + }
+10
apps/cli/src/logger.ts
··· 1 + import { configure, getConsoleSink, getLogger } from "@logtape/logtape"; 2 + 3 + await configure({ 4 + sinks: { console: getConsoleSink() }, 5 + loggers: [ 6 + { category: "@rocksky/cli", lowestLevel: "debug", sinks: ["console"] }, 7 + ], 8 + }); 9 + 10 + export const logger = getLogger("@rocksky/cli");
+2 -2
apps/cli/src/schema/album-tracks.ts
··· 1 1 import { type InferInsertModel, type InferSelectModel, sql } from "drizzle-orm"; 2 2 import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; 3 - import albums from "./albums"; 4 - import tracks from "./tracks"; 3 + import albums from "./albums.js"; 4 + import tracks from "./tracks.js"; 5 5 6 6 const albumTracks = sqliteTable("album_tracks", { 7 7 id: text("id").primaryKey().notNull(),
+2 -2
apps/cli/src/schema/artist-albums.ts
··· 1 1 import { type InferInsertModel, type InferSelectModel, sql } from "drizzle-orm"; 2 2 import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; 3 - import albums from "./albums"; 4 - import artists from "./artists"; 3 + import albums from "./albums.js"; 4 + import artists from "./artists.js"; 5 5 6 6 const artistAlbums = sqliteTable("artist_albums", { 7 7 id: text("id").primaryKey().notNull(),
+2 -2
apps/cli/src/schema/artist-tracks.ts
··· 1 1 import { type InferInsertModel, type InferSelectModel, sql } from "drizzle-orm"; 2 2 import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; 3 - import artists from "./artists"; 4 - import tracks from "./tracks"; 3 + import artists from "./artists.js"; 4 + import tracks from "./tracks.js"; 5 5 6 6 const artistTracks = sqliteTable("artist_tracks", { 7 7 id: text("id").primaryKey().notNull(),
apps/cli/src/sync.ts

This is a binary file and will not be displayed.

+26 -29
apps/cli/tsconfig.json
··· 1 1 { 2 - "compilerOptions": { 3 - "allowJs": true, 4 - "allowSyntheticDefaultImports": true, 5 - "baseUrl": "src", 6 - "declaration": true, 7 - "sourceMap": true, 8 - "esModuleInterop": true, 9 - "inlineSourceMap": false, 10 - "lib": ["esnext", "DOM"], 11 - "listEmittedFiles": false, 12 - "listFiles": false, 13 - "moduleResolution": "node", 14 - "noFallthroughCasesInSwitch": true, 15 - "pretty": true, 16 - "resolveJsonModule": true, 17 - "rootDir": ".", 18 - "skipLibCheck": true, 19 - "strict": false, 20 - "traceResolution": false, 21 - "outDir": "", 22 - "target": "esnext", 23 - "module": "esnext", 24 - "types": [ 25 - "@types/node", 26 - "@types/express", 27 - ] 28 - }, 29 - "exclude": ["node_modules", "dist", "tests"], 30 - "include": ["src", "scripts"] 2 + "compilerOptions": { 3 + "allowJs": true, 4 + "allowSyntheticDefaultImports": true, 5 + "baseUrl": "src", 6 + "declaration": true, 7 + "sourceMap": true, 8 + "esModuleInterop": true, 9 + "inlineSourceMap": false, 10 + "lib": ["esnext", "DOM"], 11 + "listEmittedFiles": false, 12 + "listFiles": false, 13 + "moduleResolution": "node", 14 + "noFallthroughCasesInSwitch": true, 15 + "pretty": true, 16 + "resolveJsonModule": true, 17 + "rootDir": ".", 18 + "skipLibCheck": true, 19 + "strict": false, 20 + "traceResolution": false, 21 + "outDir": "", 22 + "target": "esnext", 23 + "module": "esnext", 24 + "types": ["@types/node", "@types/express"], 25 + }, 26 + "exclude": ["node_modules", "dist", "tests"], 27 + "include": ["src", "scripts"], 31 28 }
+5 -31
bun.lock
··· 107 107 "rocksky": "./dist/index.js", 108 108 }, 109 109 "dependencies": { 110 - "@atcute/jetstream": "^1.1.2", 111 110 "@atproto/api": "^0.13.31", 112 111 "@atproto/common": "^0.4.6", 113 112 "@atproto/identity": "^0.4.5", ··· 116 115 "@atproto/lexicon": "^0.4.5", 117 116 "@atproto/sync": "^0.1.11", 118 117 "@atproto/syntax": "^0.3.1", 118 + "@logtape/logtape": "^1.3.6", 119 119 "@modelcontextprotocol/sdk": "^1.10.2", 120 120 "@paralleldrive/cuid2": "^3.0.6", 121 121 "@types/better-sqlite3": "^7.6.13", ··· 358 358 359 359 "@asteasolutions/zod-to-openapi": ["@asteasolutions/zod-to-openapi@7.3.4", "", { "dependencies": { "openapi3-ts": "^4.1.2" }, "peerDependencies": { "zod": "^3.20.2" } }, "sha512-/2rThQ5zPi9OzVwes6U7lK1+Yvug0iXu25olp7S0XsYmOqnyMfxH7gdSQjn/+DSOHRg7wnotwGJSyL+fBKdnEA=="], 360 360 361 - "@atcute/jetstream": ["@atcute/jetstream@1.1.2", "", { "dependencies": { "@atcute/lexicons": "^1.2.2", "@badrap/valita": "^0.4.6", "@mary-ext/event-iterator": "^1.0.0", "@mary-ext/simple-event-emitter": "^1.0.0", "partysocket": "^1.1.5", "type-fest": "^4.41.0", "yocto-queue": "^1.2.1" } }, "sha512-u6p/h2xppp7LE6W/9xErAJ6frfN60s8adZuCKtfAaaBBiiYbb1CfpzN8Uc+2qtJZNorqGvuuDb5572Jmh7yHBQ=="], 362 - 363 - "@atcute/lexicons": ["@atcute/lexicons@1.2.6", "", { "dependencies": { "@atcute/uint8array": "^1.0.6", "@atcute/util-text": "^0.0.1", "@standard-schema/spec": "^1.1.0", "esm-env": "^1.2.2" } }, "sha512-s76UQd8D+XmHIzrjD9CJ9SOOeeLPHc+sMmcj7UFakAW/dDFXc579fcRdRfuUKvXBL5v1Gs2VgDdlh/IvvQZAwA=="], 364 - 365 - "@atcute/uint8array": ["@atcute/uint8array@1.0.6", "", {}, "sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A=="], 366 - 367 - "@atcute/util-text": ["@atcute/util-text@0.0.1", "", { "dependencies": { "unicode-segmenter": "^0.14.4" } }, "sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g=="], 368 - 369 361 "@atproto-labs/did-resolver": ["@atproto-labs/did-resolver@0.1.11", "", { "dependencies": { "@atproto-labs/fetch": "0.2.2", "@atproto-labs/pipe": "0.1.0", "@atproto-labs/simple-store": "0.1.2", "@atproto-labs/simple-store-memory": "0.1.2", "@atproto/did": "0.1.5", "zod": "^3.23.8" } }, "sha512-qXNzIX2GPQnxT1gl35nv/8ErDdc4Fj/+RlJE7oyE7JGkFAPUyuY03TvKJ79SmWFsWE8wyTXEpLuphr9Da1Vhkw=="], 370 362 371 363 "@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.2", "", { "dependencies": { "@atproto-labs/pipe": "0.1.0" } }, "sha512-QyafkedbFeVaN20DYUpnY2hcArYxjdThPXbYMqOSoZhcvkrUqaw4xDND4wZB5TBD9cq2yqe9V6mcw9P4XQKQuQ=="], ··· 479 471 "@babel/traverse": ["@babel/traverse@7.28.4", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", "@babel/types": "^7.28.4", "debug": "^4.3.1" } }, "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ=="], 480 472 481 473 "@babel/types": ["@babel/types@7.28.4", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q=="], 482 - 483 - "@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="], 484 474 485 475 "@biomejs/biome": ["@biomejs/biome@2.2.5", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.2.5", "@biomejs/cli-darwin-x64": "2.2.5", "@biomejs/cli-linux-arm64": "2.2.5", "@biomejs/cli-linux-arm64-musl": "2.2.5", "@biomejs/cli-linux-x64": "2.2.5", "@biomejs/cli-linux-x64-musl": "2.2.5", "@biomejs/cli-win32-arm64": "2.2.5", "@biomejs/cli-win32-x64": "2.2.5" }, "bin": { "biome": "bin/biome" } }, "sha512-zcIi+163Rc3HtyHbEO7CjeHq8DjQRs40HsGbW6vx2WI0tg8mYQOPouhvHSyEnCBAorfYNnKdR64/IxO7xQ5faw=="], 486 476 ··· 734 724 735 725 "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], 736 726 727 + "@logtape/logtape": ["@logtape/logtape@1.3.6", "", {}, "sha512-OaK8eal8zcjB0GZbllXKgUC2T9h/GyNLQyQXjJkf1yum7SZKTWs9gs/t8NMS0kVVaSnA7bhU0Sjws/Iy4e0/IQ=="], 728 + 737 729 "@mapbox/geojson-rewind": ["@mapbox/geojson-rewind@0.5.2", "", { "dependencies": { "get-stream": "^6.0.1", "minimist": "^1.2.6" }, "bin": { "geojson-rewind": "geojson-rewind" } }, "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA=="], 738 730 739 731 "@mapbox/geojson-types": ["@mapbox/geojson-types@1.0.2", "", {}, "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw=="], ··· 751 743 "@mapbox/vector-tile": ["@mapbox/vector-tile@1.3.1", "", { "dependencies": { "@mapbox/point-geometry": "~0.1.0" } }, "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw=="], 752 744 753 745 "@mapbox/whoots-js": ["@mapbox/whoots-js@3.1.0", "", {}, "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q=="], 754 - 755 - "@mary-ext/event-iterator": ["@mary-ext/event-iterator@1.0.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-l6gCPsWJ8aRCe/s7/oCmero70kDHgIK5m4uJvYgwEYTqVxoBOIXbKr5tnkLqUHEg6mNduB4IWvms3h70Hp9ADQ=="], 756 - 757 - "@mary-ext/simple-event-emitter": ["@mary-ext/simple-event-emitter@1.0.0", "", {}, "sha512-meA/zJZKIN1RVBNEYIbjufkUrW7/tRjHH60FjolpG1ixJKo76TB208qefQLNdOVDA7uIG0CGEDuhmMirtHKLAg=="], 758 746 759 747 "@math.gl/web-mercator": ["@math.gl/web-mercator@3.6.3", "", { "dependencies": { "@babel/runtime": "^7.12.0", "gl-matrix": "^3.4.0" } }, "sha512-UVrkSOs02YLehKaehrxhAejYMurehIHPfFQvPFZmdJHglHOU4V2cCUApTVEwOksvCp161ypEqVp+9H6mGhTTcw=="], 760 748 ··· 1818 1806 1819 1807 "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], 1820 1808 1821 - "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], 1822 - 1823 1809 "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], 1824 1810 1825 1811 "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], ··· 1835 1821 "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], 1836 1822 1837 1823 "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], 1838 - 1839 - "event-target-polyfill": ["event-target-polyfill@0.0.4", "", {}, "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ=="], 1840 1824 1841 1825 "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], 1842 1826 ··· 2356 2340 2357 2341 "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], 2358 2342 2359 - "partysocket": ["partysocket@1.1.10", "", { "dependencies": { "event-target-polyfill": "^0.0.4" } }, "sha512-ACfn0P6lQuj8/AqB4L5ZDFcIEbpnIteNNObrlxqV1Ge80GTGhjuJ2sNKwNQlFzhGi4kI7fP/C1Eqh8TR78HjDQ=="], 2360 - 2361 2343 "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], 2362 2344 2363 2345 "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], ··· 2812 2794 2813 2795 "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], 2814 2796 2815 - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], 2797 + "type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], 2816 2798 2817 2799 "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], 2818 2800 ··· 2834 2816 2835 2817 "unenv": ["unenv@2.0.0-rc.14", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.1", "ohash": "^2.0.10", "pathe": "^2.0.3", "ufo": "^1.5.4" } }, "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q=="], 2836 2818 2837 - "unicode-segmenter": ["unicode-segmenter@0.14.5", "", {}, "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g=="], 2838 - 2839 2819 "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], 2840 2820 2841 2821 "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], ··· 2930 2910 2931 2911 "yesno": ["yesno@0.4.0", "", {}, "sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA=="], 2932 2912 2933 - "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], 2913 + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], 2934 2914 2935 2915 "youch": ["youch@3.2.3", "", { "dependencies": { "cookie": "^0.5.0", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-ZBcWz/uzZaQVdCvfV4uk616Bbpf2ee+F/AvuKDR5EwX/Y4v06xWdtMluqTD7+KlZdM93lLm9gMZYo0sKBS0pgw=="], 2936 2916 ··· 2941 2921 "zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="], 2942 2922 2943 2923 "zx": ["zx@8.8.4", "", { "bin": { "zx": "build/cli.js" } }, "sha512-44GcD+ZlM/v1OQtbwnSxLPcoE1ZEUICmR+RSbJZLAqfIixNLuMjLyh0DcS75OyfJ/sWYAwCWDmDvJ4hdnANAPQ=="], 2944 - 2945 - "@atcute/lexicons/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], 2946 2924 2947 2925 "@atproto-labs/fetch-node/ipaddr.js": ["ipaddr.js@2.2.0", "", {}, "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="], 2948 2926 ··· 3124 3102 3125 3103 "@storybook/addon-actions/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], 3126 3104 3127 - "@storybook/csf/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], 3128 - 3129 3105 "@storybook/csf-plugin/unplugin": ["unplugin@1.16.1", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w=="], 3130 3106 3131 3107 "@storybook/instrumenter/@vitest/utils": ["@vitest/utils@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ=="], ··· 3251 3227 "mlly/acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], 3252 3228 3253 3229 "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], 3254 - 3255 - "p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], 3256 3230 3257 3231 "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], 3258 3232