diff --git a/apps/api/src/xrpc/app/rocksky/feed/getNowPlayings.ts b/apps/api/src/xrpc/app/rocksky/feed/getNowPlayings.ts index bcee69e1..43a66f37 100644 --- a/apps/api/src/xrpc/app/rocksky/feed/getNowPlayings.ts +++ b/apps/api/src/xrpc/app/rocksky/feed/getNowPlayings.ts @@ -21,7 +21,7 @@ export default function (server: Server, ctx: Context) { Effect.map((data) => ({ data })), Effect.flatMap(presentation), Effect.retry({ times: 3 }), - Effect.timeout("120 seconds") + Effect.timeout("120 seconds"), ), }); @@ -32,7 +32,7 @@ export default function (server: Server, ctx: Context) { Effect.catchAll((err) => { console.error(err); return Effect.succeed({}); - }) + }), ); server.app.rocksky.feed.getNowPlayings({ handler: async ({ params }) => { @@ -78,11 +78,11 @@ const retrieve = ({ .leftJoin(tracks, eq(scrobbles.trackId, tracks.id)) .leftJoin(users, eq(scrobbles.userId, users.id)) .where( - sql`scrobbles.timestamp = ( - SELECT MAX(inner_s.timestamp) + sql`scrobbles.id IN ( + SELECT DISTINCT ON (inner_s.user_id) inner_s.id FROM scrobbles inner_s - WHERE inner_s.user_id = ${users.id} - )` + ORDER BY inner_s.user_id, inner_s.timestamp DESC, inner_s.id DESC + )`, ) .orderBy(desc(scrobbles.timestamp)) .limit(params.size || 20) diff --git a/apps/cli/package.json b/apps/cli/package.json index 0901b318..0836316c 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -23,7 +23,6 @@ "author": "Tsiry Sandratraina ", "license": "Apache-2.0", "dependencies": { - "@atcute/jetstream": "^1.1.2", "@atproto/api": "^0.13.31", "@atproto/common": "^0.4.6", "@atproto/identity": "^0.4.5", @@ -32,6 +31,7 @@ "@atproto/lexicon": "^0.4.5", "@atproto/sync": "^0.1.11", "@atproto/syntax": "^0.3.1", + "@logtape/logtape": "^1.3.6", "@modelcontextprotocol/sdk": "^1.10.2", "@paralleldrive/cuid2": "^3.0.6", "@types/better-sqlite3": "^7.6.13", diff --git a/apps/cli/src/cmd/scrobble.ts b/apps/cli/src/cmd/scrobble.ts index dbbb0d97..cc203613 100644 --- a/apps/cli/src/cmd/scrobble.ts +++ b/apps/cli/src/cmd/scrobble.ts @@ -1,69 +1,3 @@ -import chalk from "chalk"; -import { RockskyClient } from "client"; -import fs from "fs/promises"; -import md5 from "md5"; -import os from "os"; -import path from "path"; - export async function scrobble(track, artist, { timestamp }) { - const tokenPath = path.join(os.homedir(), ".rocksky", "token.json"); - try { - await fs.access(tokenPath); - } catch (err) { - console.error( - `You are not logged in. Please run ${chalk.greenBright( - "`rocksky login .bsky.social`" - )} first.` - ); - return; - } - - const tokenData = await fs.readFile(tokenPath, "utf-8"); - const { token } = JSON.parse(tokenData); - if (!token) { - console.error( - `You are not logged in. Please run ${chalk.greenBright( - "`rocksky login .bsky.social`" - )} first.` - ); - return; - } - - const client = new RockskyClient(token); - const apikeys = await client.getApiKeys(); - - if (!apikeys || apikeys.length === 0 || !apikeys[0].enabled) { - console.error( - `You don't have any API keys. Please create one using ${chalk.greenBright( - "`rocksky create apikey`" - )} command.` - ); - return; - } - - const signature = md5( - `api_key${ - apikeys[0].apiKey - }artist[0]${artist}methodtrack.scrobblesk${token}timestamp[0]${ - timestamp || Math.floor(Date.now() / 1000) - }track[0]${track}${apikeys[0].sharedSecret}` - ); - - const response = await client.scrobble( - apikeys[0].apiKey, - signature, - track, - artist, - timestamp - ); - - console.log( - `Scrobbled ${chalk.greenBright(track)} by ${chalk.greenBright( - artist - )} at ${chalk.greenBright( - new Date( - (timestamp || Math.floor(Date.now() / 1000)) * 1000 - ).toLocaleString() - )}` - ); + console.log(">> scrobble", track, artist, timestamp); } diff --git a/apps/cli/src/cmd/sync.ts b/apps/cli/src/cmd/sync.ts new file mode 100644 index 00000000..ef2f9aaa --- /dev/null +++ b/apps/cli/src/cmd/sync.ts @@ -0,0 +1,75 @@ +import { JetStreamClient, JetStreamEvent } from "jetstream"; +import chalk from "chalk"; +import { logger } from "logger"; + +const getEndpoint = () => { + const endpoint = process.env.JETSTREAM_SERVER + ? process.env.JETSTREAM_SERVER + : "wss://jetstream1.us-west.bsky.network/subscribe"; + + if (endpoint?.endsWith("/subscribe")) { + return endpoint; + } + + return `${endpoint}/subscribe`; +}; + +export function sync() { + const client = new JetStreamClient({ + wantedCollections: [ + "app.rocksky.scrobble", + "app.rocksky.artist", + "app.rocksky.album", + "app.rocksky.song", + ], + endpoint: getEndpoint(), + + // Optional: filter by specific DIDs + // wantedDids: ["did:plc:example123"], + + // Reconnection settings + maxReconnectAttempts: 10, + reconnectDelay: 1000, + maxReconnectDelay: 30000, + backoffMultiplier: 1.5, + + // Enable debug logging + debug: true, + }); + + client.on("open", () => { + logger.info`āœ… Connected to JetStream!`; + }); + + client.on("message", async (data) => { + const event = data as JetStreamEvent; + + if (event.kind === "commit" && event.commit) { + const { operation, collection, record, rkey } = event.commit; + const uri = `at://${event.did}/${collection}/${rkey}`; + + logger.info`\nšŸ“” New event:`; + logger.info` Operation: ${operation}`; + logger.info` Collection: ${collection}`; + logger.info` DID: ${event.did}`; + logger.info` Uri: ${uri}`; + + if (operation === "create" && record) { + console.log(JSON.stringify(record, null, 2)); + } + + logger.info` Cursor: ${event.time_us}`; + } + }); + + client.on("error", (error) => { + logger.error`āŒ Error: ${error}`; + }); + + client.on("reconnect", (data) => { + const { attempt } = data as { attempt: number }; + logger.info`šŸ”„ Reconnecting... (attempt ${attempt})`; + }); + + client.connect(); +} diff --git a/apps/cli/src/context.ts b/apps/cli/src/context.ts index 4b2dc316..c0b1693d 100644 --- a/apps/cli/src/context.ts +++ b/apps/cli/src/context.ts @@ -1,5 +1,9 @@ +import { logger } from "logger"; import drizzle from "./drizzle"; -export const context = { +export const ctx = { db: drizzle.db, + logger, }; + +export type Context = typeof ctx; diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 02790b59..cbeb4eaa 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -15,6 +15,7 @@ import { whoami } from "cmd/whoami"; import { Command } from "commander"; import version from "../package.json" assert { type: "json" }; import { login } from "./cmd/login"; +import { sync } from "cmd/sync"; const program = new Command(); @@ -22,8 +23,8 @@ program .name("rocksky") .description( `Command-line interface for Rocksky (${chalk.underline( - "https://rocksky.app" - )}) – scrobble tracks, view stats, and manage your listening history.` + "https://rocksky.app", + )}) – scrobble tracks, view stats, and manage your listening history.`, ) .version(version.version); @@ -42,7 +43,7 @@ program .command("nowplaying") .argument( "[did]", - "the DID or handle of the user to get the now playing track for." + "the DID or handle of the user to get the now playing track for.", ) .description("get the currently playing track.") .action(nowplaying); @@ -63,7 +64,7 @@ program .option("-l, --limit ", "number of results to limit") .argument( "", - "the search query, e.g., artist, album, title or account" + "the search query, e.g., artist, album, title or account", ) .description("search for tracks, albums, or accounts.") .action(search); @@ -118,4 +119,9 @@ program .description("Starts an MCP server to use with Claude or other LLMs.") .action(mcp); +program + .command("sync") + .description("Sync your local Rocksky data from AT Protocol.") + .action(sync); + program.parse(process.argv); diff --git a/apps/cli/src/jetstream.ts b/apps/cli/src/jetstream.ts index e69de29b..b406f5f9 100644 --- a/apps/cli/src/jetstream.ts +++ b/apps/cli/src/jetstream.ts @@ -0,0 +1,285 @@ +export interface JetStreamEvent { + did: string; + time_us: number; + kind: "commit" | "identity" | "account"; + commit?: { + rev: string; + operation: "create" | "update" | "delete"; + collection: string; + rkey: string; + record?: Record; + cid?: string; + }; + identity?: { + did: string; + handle?: string; + seq?: number; + time?: string; + }; + account?: { + active: boolean; + did: string; + seq: number; + time: string; + }; +} + +export interface JetStreamClientOptions { + endpoint?: string; + wantedCollections?: string[]; + wantedDids?: string[]; + maxReconnectAttempts?: number; + reconnectDelay?: number; + maxReconnectDelay?: number; + backoffMultiplier?: number; + debug?: boolean; +} + +export type JetStreamEventType = + | "open" + | "message" + | "error" + | "close" + | "reconnect"; + +export class JetStreamClient { + private ws: WebSocket | null = null; + private options: Required; + private reconnectAttempts = 0; + private reconnectTimer: number | null = null; + private isManualClose = false; + private eventHandlers: Map< + JetStreamEventType, + Set<(data?: unknown) => void> + > = new Map(); + private cursor: number | null = null; + + constructor(options: JetStreamClientOptions = {}) { + this.options = { + endpoint: + options.endpoint || "wss://jetstream1.us-east.bsky.network/subscribe", + wantedCollections: options.wantedCollections || [], + wantedDids: options.wantedDids || [], + maxReconnectAttempts: options.maxReconnectAttempts ?? Infinity, + reconnectDelay: options.reconnectDelay ?? 1000, + maxReconnectDelay: options.maxReconnectDelay ?? 30000, + backoffMultiplier: options.backoffMultiplier ?? 1.5, + debug: options.debug ?? false, + }; + + // Initialize event handler sets + ["open", "message", "error", "close", "reconnect"].forEach((event) => { + this.eventHandlers.set(event as JetStreamEventType, new Set()); + }); + } + + /** + * Register an event handler + */ + on(event: JetStreamEventType, handler: (data?: unknown) => void): this { + this.eventHandlers.get(event)?.add(handler); + return this; + } + + /** + * Remove an event handler + */ + off(event: JetStreamEventType, handler: (data?: unknown) => void): this { + this.eventHandlers.get(event)?.delete(handler); + return this; + } + + /** + * Emit an event to all registered handlers + */ + private emit(event: JetStreamEventType, data?: unknown): void { + this.eventHandlers.get(event)?.forEach((handler) => { + try { + handler(data); + } catch (error) { + this.log("error", `Handler error for ${event}:`, error); + } + }); + } + + /** + * Build the WebSocket URL with query parameters + */ + private buildUrl(): string { + const url = new URL(this.options.endpoint); + + if (this.options.wantedCollections.length > 0) { + this.options.wantedCollections.forEach((collection) => { + url.searchParams.append("wantedCollections", collection); + }); + } + + if (this.options.wantedDids.length > 0) { + this.options.wantedDids.forEach((did) => { + url.searchParams.append("wantedDids", did); + }); + } + + if (this.cursor !== null) { + url.searchParams.set("cursor", this.cursor.toString()); + } + + return url.toString(); + } + + /** + * Connect to the JetStream WebSocket + */ + connect(): void { + if (this.ws && this.ws.readyState === WebSocket.OPEN) { + this.log("warn", "Already connected"); + return; + } + + this.isManualClose = false; + const url = this.buildUrl(); + this.log("info", `Connecting to ${url}`); + + try { + this.ws = new WebSocket(url); + + this.ws.onopen = () => { + this.log("info", "Connected successfully"); + this.reconnectAttempts = 0; + this.emit("open"); + }; + + this.ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data) as JetStreamEvent; + + // Update cursor for resumption + if (data.time_us) { + this.cursor = data.time_us; + } + + this.emit("message", data); + } catch (error) { + this.log("error", "Failed to parse message:", error); + this.emit("error", { type: "parse_error", error }); + } + }; + + this.ws.onerror = (event) => { + this.log("error", "WebSocket error:", event); + this.emit("error", event); + }; + + this.ws.onclose = (event) => { + this.log("info", `Connection closed: ${event.code} ${event.reason}`); + this.emit("close", event); + + if (!this.isManualClose) { + this.scheduleReconnect(); + } + }; + } catch (error) { + this.log("error", "Failed to create WebSocket:", error); + this.emit("error", { type: "connection_error", error }); + this.scheduleReconnect(); + } + } + + /** + * Schedule a reconnection attempt with exponential backoff + */ + private scheduleReconnect(): void { + if (this.reconnectAttempts >= this.options.maxReconnectAttempts) { + this.log("error", "Max reconnection attempts reached"); + return; + } + + const delay = Math.min( + this.options.reconnectDelay * + Math.pow(this.options.backoffMultiplier, this.reconnectAttempts), + this.options.maxReconnectDelay, + ); + + this.reconnectAttempts++; + this.log( + "info", + `Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`, + ); + + this.reconnectTimer = setTimeout(() => { + this.emit("reconnect", { attempt: this.reconnectAttempts }); + this.connect(); + }, delay) as unknown as number; + } + + /** + * Manually disconnect from the WebSocket + */ + disconnect(): void { + this.isManualClose = true; + + if (this.reconnectTimer !== null) { + clearTimeout(this.reconnectTimer); + this.reconnectTimer = null; + } + + if (this.ws) { + this.ws.close(); + this.ws = null; + } + + this.log("info", "Disconnected"); + } + + /** + * Update subscription filters (requires reconnection) + */ + updateFilters(options: { + wantedCollections?: string[]; + wantedDids?: string[]; + }): void { + if (options.wantedCollections) { + this.options.wantedCollections = options.wantedCollections; + } + if (options.wantedDids) { + this.options.wantedDids = options.wantedDids; + } + + // Reconnect with new filters + if (this.ws) { + this.disconnect(); + this.connect(); + } + } + + /** + * Get current connection state + */ + get readyState(): number { + return this.ws?.readyState ?? WebSocket.CLOSED; + } + + /** + * Check if currently connected + */ + get isConnected(): boolean { + return this.ws?.readyState === WebSocket.OPEN; + } + + /** + * Get current cursor position + */ + get currentCursor(): number | null { + return this.cursor; + } + + /** + * Logging utility + */ + private log(level: "info" | "warn" | "error", ...args: unknown[]): void { + if (this.options.debug || level === "error") { + const prefix = `[JetStream ${level.toUpperCase()}]`; + console[level](prefix, ...args); + } + } +} diff --git a/apps/cli/src/logger.ts b/apps/cli/src/logger.ts new file mode 100644 index 00000000..2eb7f107 --- /dev/null +++ b/apps/cli/src/logger.ts @@ -0,0 +1,10 @@ +import { configure, getConsoleSink, getLogger } from "@logtape/logtape"; + +await configure({ + sinks: { console: getConsoleSink() }, + loggers: [ + { category: "@rocksky/cli", lowestLevel: "debug", sinks: ["console"] }, + ], +}); + +export const logger = getLogger("@rocksky/cli"); diff --git a/apps/cli/src/schema/album-tracks.ts b/apps/cli/src/schema/album-tracks.ts index 8e0ee192..74d3fbca 100644 --- a/apps/cli/src/schema/album-tracks.ts +++ b/apps/cli/src/schema/album-tracks.ts @@ -1,7 +1,7 @@ import { type InferInsertModel, type InferSelectModel, sql } from "drizzle-orm"; import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; -import albums from "./albums"; -import tracks from "./tracks"; +import albums from "./albums.js"; +import tracks from "./tracks.js"; const albumTracks = sqliteTable("album_tracks", { id: text("id").primaryKey().notNull(), diff --git a/apps/cli/src/schema/artist-albums.ts b/apps/cli/src/schema/artist-albums.ts index 31b97008..2edf86d8 100644 --- a/apps/cli/src/schema/artist-albums.ts +++ b/apps/cli/src/schema/artist-albums.ts @@ -1,7 +1,7 @@ import { type InferInsertModel, type InferSelectModel, sql } from "drizzle-orm"; import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; -import albums from "./albums"; -import artists from "./artists"; +import albums from "./albums.js"; +import artists from "./artists.js"; const artistAlbums = sqliteTable("artist_albums", { id: text("id").primaryKey().notNull(), diff --git a/apps/cli/src/schema/artist-tracks.ts b/apps/cli/src/schema/artist-tracks.ts index 1614d5f7..c38d33e2 100644 --- a/apps/cli/src/schema/artist-tracks.ts +++ b/apps/cli/src/schema/artist-tracks.ts @@ -1,7 +1,7 @@ import { type InferInsertModel, type InferSelectModel, sql } from "drizzle-orm"; import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"; -import artists from "./artists"; -import tracks from "./tracks"; +import artists from "./artists.js"; +import tracks from "./tracks.js"; const artistTracks = sqliteTable("artist_tracks", { id: text("id").primaryKey().notNull(), diff --git a/apps/cli/src/sync.ts b/apps/cli/src/sync.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index 5f99a71a..3a3a3e5b 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -1,31 +1,28 @@ { - "compilerOptions": { - "allowJs": true, - "allowSyntheticDefaultImports": true, - "baseUrl": "src", - "declaration": true, - "sourceMap": true, - "esModuleInterop": true, - "inlineSourceMap": false, - "lib": ["esnext", "DOM"], - "listEmittedFiles": false, - "listFiles": false, - "moduleResolution": "node", - "noFallthroughCasesInSwitch": true, - "pretty": true, - "resolveJsonModule": true, - "rootDir": ".", - "skipLibCheck": true, - "strict": false, - "traceResolution": false, - "outDir": "", - "target": "esnext", - "module": "esnext", - "types": [ - "@types/node", - "@types/express", - ] - }, - "exclude": ["node_modules", "dist", "tests"], - "include": ["src", "scripts"] + "compilerOptions": { + "allowJs": true, + "allowSyntheticDefaultImports": true, + "baseUrl": "src", + "declaration": true, + "sourceMap": true, + "esModuleInterop": true, + "inlineSourceMap": false, + "lib": ["esnext", "DOM"], + "listEmittedFiles": false, + "listFiles": false, + "moduleResolution": "node", + "noFallthroughCasesInSwitch": true, + "pretty": true, + "resolveJsonModule": true, + "rootDir": ".", + "skipLibCheck": true, + "strict": false, + "traceResolution": false, + "outDir": "", + "target": "esnext", + "module": "esnext", + "types": ["@types/node", "@types/express"], + }, + "exclude": ["node_modules", "dist", "tests"], + "include": ["src", "scripts"], } diff --git a/bun.lock b/bun.lock index 3534ac4b..d27d7a0a 100644 --- a/bun.lock +++ b/bun.lock @@ -107,7 +107,6 @@ "rocksky": "./dist/index.js", }, "dependencies": { - "@atcute/jetstream": "^1.1.2", "@atproto/api": "^0.13.31", "@atproto/common": "^0.4.6", "@atproto/identity": "^0.4.5", @@ -116,6 +115,7 @@ "@atproto/lexicon": "^0.4.5", "@atproto/sync": "^0.1.11", "@atproto/syntax": "^0.3.1", + "@logtape/logtape": "^1.3.6", "@modelcontextprotocol/sdk": "^1.10.2", "@paralleldrive/cuid2": "^3.0.6", "@types/better-sqlite3": "^7.6.13", @@ -358,14 +358,6 @@ "@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=="], - "@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=="], - - "@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=="], - - "@atcute/uint8array": ["@atcute/uint8array@1.0.6", "", {}, "sha512-ucfRBQc7BFT8n9eCyGOzDHEMKF/nZwhS2pPao4Xtab1ML3HdFYcX2DM1tadCzas85QTGxHe5urnUAAcNKGRi9A=="], - - "@atcute/util-text": ["@atcute/util-text@0.0.1", "", { "dependencies": { "unicode-segmenter": "^0.14.4" } }, "sha512-t1KZqvn0AYy+h2KcJyHnKF9aEqfRfMUmyY8j1ELtAEIgqN9CxINAjxnoRCJIFUlvWzb+oY3uElQL/Vyk3yss0g=="], - "@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=="], "@atproto-labs/fetch": ["@atproto-labs/fetch@0.2.2", "", { "dependencies": { "@atproto-labs/pipe": "0.1.0" } }, "sha512-QyafkedbFeVaN20DYUpnY2hcArYxjdThPXbYMqOSoZhcvkrUqaw4xDND4wZB5TBD9cq2yqe9V6mcw9P4XQKQuQ=="], @@ -480,8 +472,6 @@ "@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=="], - "@badrap/valita": ["@badrap/valita@0.4.6", "", {}, "sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg=="], - "@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=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.2.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MYT+nZ38wEIWVcL5xLyOhYQQ7nlWD0b/4mgATW2c8dvq7R4OQjt/XGXFkXrmtWmQofaIM14L7V8qIz/M+bx5QQ=="], @@ -734,6 +724,8 @@ "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + "@logtape/logtape": ["@logtape/logtape@1.3.6", "", {}, "sha512-OaK8eal8zcjB0GZbllXKgUC2T9h/GyNLQyQXjJkf1yum7SZKTWs9gs/t8NMS0kVVaSnA7bhU0Sjws/Iy4e0/IQ=="], + "@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=="], "@mapbox/geojson-types": ["@mapbox/geojson-types@1.0.2", "", {}, "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw=="], @@ -752,10 +744,6 @@ "@mapbox/whoots-js": ["@mapbox/whoots-js@3.1.0", "", {}, "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q=="], - "@mary-ext/event-iterator": ["@mary-ext/event-iterator@1.0.0", "", { "dependencies": { "yocto-queue": "^1.2.1" } }, "sha512-l6gCPsWJ8aRCe/s7/oCmero70kDHgIK5m4uJvYgwEYTqVxoBOIXbKr5tnkLqUHEg6mNduB4IWvms3h70Hp9ADQ=="], - - "@mary-ext/simple-event-emitter": ["@mary-ext/simple-event-emitter@1.0.0", "", {}, "sha512-meA/zJZKIN1RVBNEYIbjufkUrW7/tRjHH60FjolpG1ixJKo76TB208qefQLNdOVDA7uIG0CGEDuhmMirtHKLAg=="], - "@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=="], "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], @@ -1818,8 +1806,6 @@ "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], - "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], - "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], @@ -1836,8 +1822,6 @@ "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - "event-target-polyfill": ["event-target-polyfill@0.0.4", "", {}, "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ=="], - "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], @@ -2356,8 +2340,6 @@ "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - "partysocket": ["partysocket@1.1.10", "", { "dependencies": { "event-target-polyfill": "^0.0.4" } }, "sha512-ACfn0P6lQuj8/AqB4L5ZDFcIEbpnIteNNObrlxqV1Ge80GTGhjuJ2sNKwNQlFzhGi4kI7fP/C1Eqh8TR78HjDQ=="], - "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], @@ -2812,7 +2794,7 @@ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], "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=="], @@ -2834,8 +2816,6 @@ "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=="], - "unicode-segmenter": ["unicode-segmenter@0.14.5", "", {}, "sha512-jHGmj2LUuqDcX3hqY12Ql+uhUTn8huuxNZGq7GvtF6bSybzH3aFgedYu/KTzQStEgt1Ra2F3HxadNXsNjb3m3g=="], - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], @@ -2930,7 +2910,7 @@ "yesno": ["yesno@0.4.0", "", {}, "sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA=="], - "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "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=="], @@ -2942,8 +2922,6 @@ "zx": ["zx@8.8.4", "", { "bin": { "zx": "build/cli.js" } }, "sha512-44GcD+ZlM/v1OQtbwnSxLPcoE1ZEUICmR+RSbJZLAqfIixNLuMjLyh0DcS75OyfJ/sWYAwCWDmDvJ4hdnANAPQ=="], - "@atcute/lexicons/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@atproto-labs/fetch-node/ipaddr.js": ["ipaddr.js@2.2.0", "", {}, "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA=="], "@atproto-labs/fetch-node/undici": ["undici@6.22.0", "", {}, "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw=="], @@ -3124,8 +3102,6 @@ "@storybook/addon-actions/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], - "@storybook/csf/type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], - "@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=="], "@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=="], @@ -3252,8 +3228,6 @@ "mlly/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - "p-limit/yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "pkg-types/pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "posthog-js/fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="],