+6
-6
apps/api/src/xrpc/app/rocksky/feed/getNowPlayings.ts
+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
+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
-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
+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
+5
-1
apps/cli/src/context.ts
+10
-4
apps/cli/src/index.ts
+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
+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
+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
+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
+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
+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
apps/cli/src/sync.ts
This is a binary file and will not be displayed.
+26
-29
apps/cli/tsconfig.json
+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
+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