+20
.env.example
+20
.env.example
···
1
+
# Comma-separated list of users who can use the bot (delete var if you want everyone to be able to use it)
2
+
AUTHORIZED_USERS=""
3
+
4
+
# PDS service URL (optional)
5
+
SERVICE="https://bsky.social"
6
+
7
+
DB_PATH="data/sqlite.db"
8
+
GEMINI_MODEL="gemini-2.5-flash"
9
+
10
+
ADMIN_DID=""
11
+
ADMIN_HANDLE=""
12
+
13
+
DID=""
14
+
HANDLE=""
15
+
16
+
# https://bsky.app/settings/app-passwords
17
+
BSKY_PASSWORD=""
18
+
19
+
# https://aistudio.google.com/apikey
20
+
GEMINI_API_KEY=""
+37
.gitignore
+37
.gitignore
···
1
+
# dependencies (bun install)
2
+
node_modules
3
+
4
+
# output
5
+
out
6
+
dist
7
+
*.tgz
8
+
9
+
# code coverage
10
+
coverage
11
+
*.lcov
12
+
13
+
# logs
14
+
logs
15
+
_.log
16
+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
17
+
18
+
# dotenv environment variable files
19
+
.env
20
+
.env.development.local
21
+
.env.test.local
22
+
.env.production.local
23
+
.env.local
24
+
25
+
# caches
26
+
.eslintcache
27
+
.cache
28
+
*.tsbuildinfo
29
+
30
+
# IntelliJ based IDEs
31
+
.idea
32
+
33
+
# Finder (MacOS) folder config
34
+
.DS_Store
35
+
36
+
# Database
37
+
data
+11
Dockerfile
+11
Dockerfile
+16
README.md
+16
README.md
···
1
+
# Aero
2
+
3
+
A simple Bluesky bot to make sense of the noise, with responses powered by Gemini, similar to Grok. Built with the [@skyware/bot](https://github.com/skyware-js/bot) library.
4
+
5
+
## How to Use
6
+
7
+
- Find a post you want to ask about
8
+
- Start a conversation by sending a link to the post and your initial query to the [`@aero.indexx.dev`](https://bsky.app/profile/did:plc:brtrdeexwvywvennyptpwcnu) account.
9
+
..after a few seconds, you'll get a response to your query!
10
+
11
+
**For any further queries:**
12
+
13
+
- You don't need to resend the post link, just send the message like normal. Ever want to switch posts? Just send a new post link and your context window will reset.
14
+
- You can ask up to 15 queries per post link, and all messages prior to the new post link are ignored.
15
+
16
+
All messages are stored in the database just to make the process simpler so I don't have to deal with cursors or accidentally including messages that shouldn't be included in the context window.
+270
bun.lock
+270
bun.lock
···
1
+
{
2
+
"lockfileVersion": 1,
3
+
"workspaces": {
4
+
"": {
5
+
"name": "bsky-echo",
6
+
"dependencies": {
7
+
"@atproto/syntax": "^0.4.0",
8
+
"@google/genai": "^1.11.0",
9
+
"@skyware/bot": "^0.3.12",
10
+
"@types/js-yaml": "^4.0.9",
11
+
"consola": "^3.4.2",
12
+
"drizzle-orm": "^0.44.4",
13
+
"js-yaml": "^4.1.0",
14
+
"zod": "^4.0.14",
15
+
},
16
+
"devDependencies": {
17
+
"@types/bun": "^1.2.19",
18
+
"drizzle-kit": "^0.31.4",
19
+
},
20
+
"peerDependencies": {
21
+
"typescript": "^5.8.3",
22
+
},
23
+
},
24
+
},
25
+
"packages": {
26
+
"@atcute/atproto": ["@atcute/atproto@3.1.1", "", { "dependencies": { "@atcute/lexicons": "^1.1.0" } }, "sha512-D+RLTIPF0xLu7BPZY8KSewAPemJFh+3n3zeQ3ROsLxbTtCHbrTDMAmAFexaVRAPGcPYrwXaBUlv7yZjScJolMg=="],
27
+
28
+
"@atcute/bluesky": ["@atcute/bluesky@1.0.15", "", { "peerDependencies": { "@atcute/client": "^1.0.0 || ^2.0.0" } }, "sha512-+EFiybmKQ97aBAgtaD+cKRJER5AMn3cZMkEwEg/pDdWyzxYJ9m1UgemmLdTgI8VrxPufKqdXS2nl7uO7TY6BPA=="],
29
+
30
+
"@atcute/bluesky-richtext-builder": ["@atcute/bluesky-richtext-builder@1.0.2", "", { "peerDependencies": { "@atcute/bluesky": "^1.0.0", "@atcute/client": "^1.0.0 || ^2.0.0" } }, "sha512-sa+9B5Ygb1GcWeMpav9RVBRdFLL5snZEoFFF2RkTaNr61m/cLd5lk97QJs+t9LXUEl5cfHS3jXujywFrGXZj9w=="],
31
+
32
+
"@atcute/car": ["@atcute/car@1.1.1", "", { "dependencies": { "@atcute/cbor": "^1.0.6", "@atcute/cid": "^1.0.2", "@atcute/varint": "^1.0.1" } }, "sha512-j6HY//ttIFCbOioDlEowKn2WOGeNavJenZkAP+wWIhsbRlK+V4+TpnJ38IX/VYfMpQHrKweh3W94wRCYp6L5Zg=="],
33
+
34
+
"@atcute/cbor": ["@atcute/cbor@1.0.7", "", { "dependencies": { "@atcute/cid": "^1.0.3", "@atcute/multibase": "^1.0.0" } }, "sha512-z3chucgCqjAN36ySvUVl1VSwtGME4CDS173eaaEfiTSpRIQ6ewKpKlkzapLUNqtLU9iBx884b9c2j6kjEyn1XA=="],
35
+
36
+
"@atcute/cid": ["@atcute/cid@1.0.3", "", { "dependencies": { "@atcute/multibase": "^1.0.0", "@atcute/varint": "^1.0.1" } }, "sha512-BZbs+Xt0yMci0I2dLqqYsN76ua8lkMk/HQfEIKr7g2XMBlSc0XNCXfZdbAWPwiCK/NuGaPBocYMRwApd4dF2Qg=="],
37
+
38
+
"@atcute/client": ["@atcute/client@2.0.9", "", {}, "sha512-QNDm9gMP6x9LY77ArwY+urQOBtQW74/onEAz42c40JxRm6Rl9K9cU4ROvNKJ+5cpVmEm1sthEWVRmDr5CSZENA=="],
39
+
40
+
"@atcute/lexicons": ["@atcute/lexicons@1.1.0", "", { "dependencies": { "esm-env": "^1.2.2" } }, "sha512-LFqwnria78xLYb62Ri/+WwQpUTgZp2DuyolNGIIOV1dpiKhFFFh//nscHMA6IExFLQRqWDs3tTjy7zv0h3sf1Q=="],
41
+
42
+
"@atcute/multibase": ["@atcute/multibase@1.1.4", "", { "dependencies": { "@atcute/uint8array": "^1.0.2" } }, "sha512-NUf5AeeSOmuZHGU+4GAaMtISJoG+ZHtW/vUVA4lK/YDt/7LODAW0Fd0NNIIUPVUoW0xJS6zSEIWvwLLuxmEHhA=="],
43
+
44
+
"@atcute/ozone": ["@atcute/ozone@1.0.12", "", { "peerDependencies": { "@atcute/bluesky": "^1.0.0", "@atcute/client": "^1.0.0 || ^2.0.0" } }, "sha512-eogx/FCF6X3WTwAPxgG8RcrziuOUcJvMu+qHodeVcLSQ7QJvw2H/Q5V0HpnZegUOY5aRGKb5RvLk2SeZq3LCeA=="],
45
+
46
+
"@atcute/uint8array": ["@atcute/uint8array@1.0.3", "", {}, "sha512-M/K+ihiVW8Pl2PFLzaC4E3l4JaZ1IH05Q0AbPWUC4cVHnd/gZ/1kAF5ngdtGvJeDMirHZ2VAy7OmAsPwR/2nlA=="],
47
+
48
+
"@atcute/varint": ["@atcute/varint@1.0.2", "", {}, "sha512-0O31hePzzr4O3NGWHUKKOyta6CGSL+AtN8iir8grGxu9jXyI7DBARlw6PbgKA6uTAvsXdpmRmF8MX+p0TsLnNg=="],
49
+
50
+
"@atproto/syntax": ["@atproto/syntax@0.4.0", "", {}, "sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA=="],
51
+
52
+
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
53
+
54
+
"@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="],
55
+
56
+
"@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="],
57
+
58
+
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.8", "", { "os": "aix", "cpu": "ppc64" }, "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA=="],
59
+
60
+
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.8", "", { "os": "android", "cpu": "arm" }, "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw=="],
61
+
62
+
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.8", "", { "os": "android", "cpu": "arm64" }, "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w=="],
63
+
64
+
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.8", "", { "os": "android", "cpu": "x64" }, "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA=="],
65
+
66
+
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw=="],
67
+
68
+
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg=="],
69
+
70
+
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.8", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA=="],
71
+
72
+
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw=="],
73
+
74
+
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.8", "", { "os": "linux", "cpu": "arm" }, "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg=="],
75
+
76
+
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w=="],
77
+
78
+
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.8", "", { "os": "linux", "cpu": "ia32" }, "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg=="],
79
+
80
+
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.8", "", { "os": "linux", "cpu": "none" }, "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ=="],
81
+
82
+
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.8", "", { "os": "linux", "cpu": "none" }, "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw=="],
83
+
84
+
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ=="],
85
+
86
+
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.8", "", { "os": "linux", "cpu": "none" }, "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg=="],
87
+
88
+
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg=="],
89
+
90
+
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.8", "", { "os": "linux", "cpu": "x64" }, "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ=="],
91
+
92
+
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.8", "", { "os": "none", "cpu": "arm64" }, "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw=="],
93
+
94
+
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.8", "", { "os": "none", "cpu": "x64" }, "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg=="],
95
+
96
+
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.8", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ=="],
97
+
98
+
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.8", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ=="],
99
+
100
+
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.8", "", { "os": "none", "cpu": "arm64" }, "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg=="],
101
+
102
+
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.8", "", { "os": "sunos", "cpu": "x64" }, "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w=="],
103
+
104
+
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ=="],
105
+
106
+
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.8", "", { "os": "win32", "cpu": "ia32" }, "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg=="],
107
+
108
+
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="],
109
+
110
+
"@google/genai": ["@google/genai@1.11.0", "", { "dependencies": { "google-auth-library": "^9.14.2", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.11.0" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-4XFAHCvU91ewdWOU3RUdSeXpDuZRJHNYLqT9LKw7WqPjRQcEJvVU+VOU49ocruaSp8VuLKMecl0iadlQK+Zgfw=="],
111
+
112
+
"@skyware/bot": ["@skyware/bot@0.3.12", "", { "dependencies": { "@atcute/bluesky": "^1.0.7", "@atcute/bluesky-richtext-builder": "^1.0.1", "@atcute/client": "^2.0.3", "@atcute/ozone": "^1.0.5", "quick-lru": "^7.0.0", "rate-limit-threshold": "^0.1.5" }, "optionalDependencies": { "@skyware/firehose": "^0.3.2", "@skyware/jetstream": "^0.2.2" } }, "sha512-5OqTtwItYsBFMh0nwrxfsqgXrvRaJzg1P+ghMV4rlRGwHhdRgBJcnYQYgUqqREFcB247yGo73LNyqq7kHEwV7Q=="],
113
+
114
+
"@skyware/firehose": ["@skyware/firehose@0.3.2", "", { "dependencies": { "@atcute/car": "^1.1.0", "@atcute/cbor": "^1.0.3", "ws": "^8.16.0" } }, "sha512-CmRaw3lFPEd9euFGV+K/n/TF/o0Rre87oJP5pswC8IExj/qQnWVoncIulAJbL3keUCm5mlt49jCiiqfQXVjigg=="],
115
+
116
+
"@skyware/jetstream": ["@skyware/jetstream@0.2.5", "", { "dependencies": { "@atcute/atproto": "^3.1.0", "@atcute/bluesky": "^3.1.4", "@atcute/lexicons": "^1.1.0", "partysocket": "^1.1.3", "tiny-emitter": "^2.1.0" } }, "sha512-fM/zs03DLwqRyzZZJFWN20e76KrdqIp97Tlm8Cek+vxn96+tu5d/fx79V6H85L0QN6HvGiX2l9A8hWFqHvYlOA=="],
117
+
118
+
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
119
+
120
+
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
121
+
122
+
"@types/node": ["@types/node@24.0.15", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA=="],
123
+
124
+
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
125
+
126
+
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
127
+
128
+
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
129
+
130
+
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
131
+
132
+
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
133
+
134
+
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
135
+
136
+
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
137
+
138
+
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
139
+
140
+
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
141
+
142
+
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
143
+
144
+
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
145
+
146
+
"drizzle-kit": ["drizzle-kit@0.31.4", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA=="],
147
+
148
+
"drizzle-orm": ["drizzle-orm@0.44.4", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-ZyzKFpTC/Ut3fIqc2c0dPZ6nhchQXriTsqTNs4ayRgl6sZcFlMs9QZKPSHXK4bdOf41GHGWf+FrpcDDYwW+W6Q=="],
149
+
150
+
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
151
+
152
+
"esbuild": ["esbuild@0.25.8", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.8", "@esbuild/android-arm": "0.25.8", "@esbuild/android-arm64": "0.25.8", "@esbuild/android-x64": "0.25.8", "@esbuild/darwin-arm64": "0.25.8", "@esbuild/darwin-x64": "0.25.8", "@esbuild/freebsd-arm64": "0.25.8", "@esbuild/freebsd-x64": "0.25.8", "@esbuild/linux-arm": "0.25.8", "@esbuild/linux-arm64": "0.25.8", "@esbuild/linux-ia32": "0.25.8", "@esbuild/linux-loong64": "0.25.8", "@esbuild/linux-mips64el": "0.25.8", "@esbuild/linux-ppc64": "0.25.8", "@esbuild/linux-riscv64": "0.25.8", "@esbuild/linux-s390x": "0.25.8", "@esbuild/linux-x64": "0.25.8", "@esbuild/netbsd-arm64": "0.25.8", "@esbuild/netbsd-x64": "0.25.8", "@esbuild/openbsd-arm64": "0.25.8", "@esbuild/openbsd-x64": "0.25.8", "@esbuild/openharmony-arm64": "0.25.8", "@esbuild/sunos-x64": "0.25.8", "@esbuild/win32-arm64": "0.25.8", "@esbuild/win32-ia32": "0.25.8", "@esbuild/win32-x64": "0.25.8" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-vVC0USHGtMi8+R4Kz8rt6JhEWLxsv9Rnu/lGYbPR8u47B+DCBksq9JarW0zOO7bs37hyOK1l2/oqtbciutL5+Q=="],
153
+
154
+
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
155
+
156
+
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
157
+
158
+
"event-target-polyfill": ["event-target-polyfill@0.0.4", "", {}, "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ=="],
159
+
160
+
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
161
+
162
+
"gaxios": ["gaxios@6.7.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "is-stream": "^2.0.0", "node-fetch": "^2.6.9", "uuid": "^9.0.1" } }, "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ=="],
163
+
164
+
"gcp-metadata": ["gcp-metadata@6.1.1", "", { "dependencies": { "gaxios": "^6.1.1", "google-logging-utils": "^0.0.2", "json-bigint": "^1.0.0" } }, "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A=="],
165
+
166
+
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
167
+
168
+
"google-auth-library": ["google-auth-library@9.15.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^6.1.1", "gcp-metadata": "^6.1.0", "gtoken": "^7.0.0", "jws": "^4.0.0" } }, "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng=="],
169
+
170
+
"google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
171
+
172
+
"gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="],
173
+
174
+
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
175
+
176
+
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
177
+
178
+
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
179
+
180
+
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
181
+
182
+
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
183
+
184
+
"jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
185
+
186
+
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
187
+
188
+
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
189
+
190
+
"partysocket": ["partysocket@1.1.4", "", { "dependencies": { "event-target-polyfill": "^0.0.4" } }, "sha512-jXP7PFj2h5/v4UjDS8P7MZy6NJUQ7sspiFyxL4uc/+oKOL+KdtXzHnTV8INPGxBrLTXgalyG3kd12Qm7WrYc3A=="],
191
+
192
+
"quick-lru": ["quick-lru@7.0.1", "", {}, "sha512-kLjThirJMkWKutUKbZ8ViqFc09tDQhlbQo2MNuVeLWbRauqYP96Sm6nzlQ24F0HFjUNZ4i9+AgldJ9H6DZXi7g=="],
193
+
194
+
"rate-limit-threshold": ["rate-limit-threshold@0.1.5", "", {}, "sha512-75vpvXC/ZqQJrFDp0dVtfoXZi8kxQP2eBuxVYFvGDfnHhcgE+ZG870u4ItQhWQh54Y6nNwOaaq5g3AL9n27lTg=="],
195
+
196
+
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
197
+
198
+
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
199
+
200
+
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
201
+
202
+
"source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
203
+
204
+
"tiny-emitter": ["tiny-emitter@2.1.0", "", {}, "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="],
205
+
206
+
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
207
+
208
+
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
209
+
210
+
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
211
+
212
+
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
213
+
214
+
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
215
+
216
+
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
217
+
218
+
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
219
+
220
+
"zod": ["zod@4.0.14", "", {}, "sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw=="],
221
+
222
+
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
223
+
224
+
"@skyware/jetstream/@atcute/bluesky": ["@atcute/bluesky@3.1.5", "", { "dependencies": { "@atcute/atproto": "^3.1.1", "@atcute/lexicons": "^1.1.0" } }, "sha512-OJO1HOqRZmpSQ2W2QSbgGIk301JUX7rmLV8LYqQGxsbpNJOLNJ8//vcD4Ag4WsxTRm+Z+vEUZ4qWXnNsZlgXXg=="],
225
+
226
+
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
227
+
228
+
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
229
+
230
+
"@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
231
+
232
+
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
233
+
234
+
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
235
+
236
+
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
237
+
238
+
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
239
+
240
+
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
241
+
242
+
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
243
+
244
+
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
245
+
246
+
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
247
+
248
+
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
249
+
250
+
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
251
+
252
+
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
253
+
254
+
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
255
+
256
+
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
257
+
258
+
"@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
259
+
260
+
"@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
261
+
262
+
"@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
263
+
264
+
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
265
+
266
+
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
267
+
268
+
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
269
+
}
270
+
}
+7
drizzle.config.ts
+7
drizzle.config.ts
+22
drizzle/0000_black_micromax.sql
+22
drizzle/0000_black_micromax.sql
···
1
+
CREATE TABLE `conversations` (
2
+
`id` text NOT NULL,
3
+
`did` text NOT NULL,
4
+
`post_uri` text NOT NULL,
5
+
`revision` text NOT NULL,
6
+
`created_at` integer DEFAULT CURRENT_TIMESTAMP NOT NULL,
7
+
`last_active` integer DEFAULT CURRENT_TIMESTAMP NOT NULL
8
+
);
9
+
--> statement-breakpoint
10
+
CREATE UNIQUE INDEX `conversations_id_unique` ON `conversations` (`id`);--> statement-breakpoint
11
+
CREATE UNIQUE INDEX `conversations_did_unique` ON `conversations` (`did`);--> statement-breakpoint
12
+
CREATE TABLE `messages` (
13
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
14
+
`conversation_id` text NOT NULL,
15
+
`revision` text NOT NULL,
16
+
`did` text NOT NULL,
17
+
`post_uri` text NOT NULL,
18
+
`text` text NOT NULL,
19
+
`created_at` integer DEFAULT CURRENT_TIMESTAMP NOT NULL,
20
+
FOREIGN KEY (`conversation_id`) REFERENCES `conversations`(`id`) ON UPDATE no action ON DELETE no action,
21
+
FOREIGN KEY (`revision`) REFERENCES `conversations`(`revision`) ON UPDATE no action ON DELETE no action
22
+
);
+174
drizzle/meta/0000_snapshot.json
+174
drizzle/meta/0000_snapshot.json
···
1
+
{
2
+
"version": "6",
3
+
"dialect": "sqlite",
4
+
"id": "77441a9c-7dba-4cc4-b9db-5720373932d8",
5
+
"prevId": "00000000-0000-0000-0000-000000000000",
6
+
"tables": {
7
+
"conversations": {
8
+
"name": "conversations",
9
+
"columns": {
10
+
"id": {
11
+
"name": "id",
12
+
"type": "text",
13
+
"primaryKey": false,
14
+
"notNull": true,
15
+
"autoincrement": false
16
+
},
17
+
"did": {
18
+
"name": "did",
19
+
"type": "text",
20
+
"primaryKey": false,
21
+
"notNull": true,
22
+
"autoincrement": false
23
+
},
24
+
"post_uri": {
25
+
"name": "post_uri",
26
+
"type": "text",
27
+
"primaryKey": false,
28
+
"notNull": true,
29
+
"autoincrement": false
30
+
},
31
+
"revision": {
32
+
"name": "revision",
33
+
"type": "text",
34
+
"primaryKey": false,
35
+
"notNull": true,
36
+
"autoincrement": false
37
+
},
38
+
"created_at": {
39
+
"name": "created_at",
40
+
"type": "integer",
41
+
"primaryKey": false,
42
+
"notNull": true,
43
+
"autoincrement": false,
44
+
"default": "CURRENT_TIMESTAMP"
45
+
},
46
+
"last_active": {
47
+
"name": "last_active",
48
+
"type": "integer",
49
+
"primaryKey": false,
50
+
"notNull": true,
51
+
"autoincrement": false,
52
+
"default": "CURRENT_TIMESTAMP"
53
+
}
54
+
},
55
+
"indexes": {
56
+
"conversations_id_unique": {
57
+
"name": "conversations_id_unique",
58
+
"columns": [
59
+
"id"
60
+
],
61
+
"isUnique": true
62
+
},
63
+
"conversations_did_unique": {
64
+
"name": "conversations_did_unique",
65
+
"columns": [
66
+
"did"
67
+
],
68
+
"isUnique": true
69
+
}
70
+
},
71
+
"foreignKeys": {},
72
+
"compositePrimaryKeys": {},
73
+
"uniqueConstraints": {},
74
+
"checkConstraints": {}
75
+
},
76
+
"messages": {
77
+
"name": "messages",
78
+
"columns": {
79
+
"id": {
80
+
"name": "id",
81
+
"type": "integer",
82
+
"primaryKey": true,
83
+
"notNull": true,
84
+
"autoincrement": true
85
+
},
86
+
"conversation_id": {
87
+
"name": "conversation_id",
88
+
"type": "text",
89
+
"primaryKey": false,
90
+
"notNull": true,
91
+
"autoincrement": false
92
+
},
93
+
"revision": {
94
+
"name": "revision",
95
+
"type": "text",
96
+
"primaryKey": false,
97
+
"notNull": true,
98
+
"autoincrement": false
99
+
},
100
+
"did": {
101
+
"name": "did",
102
+
"type": "text",
103
+
"primaryKey": false,
104
+
"notNull": true,
105
+
"autoincrement": false
106
+
},
107
+
"post_uri": {
108
+
"name": "post_uri",
109
+
"type": "text",
110
+
"primaryKey": false,
111
+
"notNull": true,
112
+
"autoincrement": false
113
+
},
114
+
"text": {
115
+
"name": "text",
116
+
"type": "text",
117
+
"primaryKey": false,
118
+
"notNull": true,
119
+
"autoincrement": false
120
+
},
121
+
"created_at": {
122
+
"name": "created_at",
123
+
"type": "integer",
124
+
"primaryKey": false,
125
+
"notNull": true,
126
+
"autoincrement": false,
127
+
"default": "CURRENT_TIMESTAMP"
128
+
}
129
+
},
130
+
"indexes": {},
131
+
"foreignKeys": {
132
+
"messages_conversation_id_conversations_id_fk": {
133
+
"name": "messages_conversation_id_conversations_id_fk",
134
+
"tableFrom": "messages",
135
+
"tableTo": "conversations",
136
+
"columnsFrom": [
137
+
"conversation_id"
138
+
],
139
+
"columnsTo": [
140
+
"id"
141
+
],
142
+
"onDelete": "no action",
143
+
"onUpdate": "no action"
144
+
},
145
+
"messages_revision_conversations_revision_fk": {
146
+
"name": "messages_revision_conversations_revision_fk",
147
+
"tableFrom": "messages",
148
+
"tableTo": "conversations",
149
+
"columnsFrom": [
150
+
"revision"
151
+
],
152
+
"columnsTo": [
153
+
"revision"
154
+
],
155
+
"onDelete": "no action",
156
+
"onUpdate": "no action"
157
+
}
158
+
},
159
+
"compositePrimaryKeys": {},
160
+
"uniqueConstraints": {},
161
+
"checkConstraints": {}
162
+
}
163
+
},
164
+
"views": {},
165
+
"enums": {},
166
+
"_meta": {
167
+
"schemas": {},
168
+
"tables": {},
169
+
"columns": {}
170
+
},
171
+
"internal": {
172
+
"indexes": {}
173
+
}
174
+
}
+13
drizzle/meta/_journal.json
+13
drizzle/meta/_journal.json
+36
package.json
+36
package.json
···
1
+
{
2
+
"name": "bsky-aero",
3
+
"description": "A simple Bluesky bot to make sense of all the noise, with responses powered by Gemini.",
4
+
"module": "index.ts",
5
+
"type": "module",
6
+
"scripts": {
7
+
"start": "bun run src/index.ts",
8
+
"dev": "bun run --watch src/index.ts",
9
+
"db:generate": "bunx drizzle-kit generate --dialect sqlite --schema ./src/db/schema.ts",
10
+
"db:migrate": "bun run src/db/migrate.ts"
11
+
},
12
+
"devDependencies": {
13
+
"@types/bun": "^1.2.19",
14
+
"drizzle-kit": "^0.31.4"
15
+
},
16
+
"peerDependencies": {
17
+
"typescript": "^5.8.3"
18
+
},
19
+
"dependencies": {
20
+
"@atproto/syntax": "^0.4.0",
21
+
"@google/genai": "^1.11.0",
22
+
"@skyware/bot": "^0.3.12",
23
+
"@types/js-yaml": "^4.0.9",
24
+
"consola": "^3.4.2",
25
+
"drizzle-orm": "^0.44.4",
26
+
"js-yaml": "^4.1.0",
27
+
"zod": "^4.0.14"
28
+
},
29
+
"repository": {
30
+
"url": "https://github.com/indexxing/echo"
31
+
},
32
+
"author": {
33
+
"name": "Index",
34
+
"email": "contact@indexx.dev"
35
+
}
36
+
}
+23
src/core.ts
+23
src/core.ts
···
1
+
import { GoogleGenAI } from "@google/genai";
2
+
import { Bot } from "@skyware/bot";
3
+
import { env } from "./env";
4
+
5
+
export const bot = new Bot({
6
+
service: env.SERVICE,
7
+
emitChatEvents: true,
8
+
});
9
+
10
+
export const ai = new GoogleGenAI({
11
+
apiKey: env.GEMINI_API_KEY,
12
+
});
13
+
14
+
export const UNAUTHORIZED_MESSAGE =
15
+
"I can’t make sense of your noise just yet. You’ll need to be whitelisted before I can help.";
16
+
17
+
export const SUPPORTED_FUNCTION_CALLS = [
18
+
"search_posts",
19
+
] as const;
20
+
21
+
export const MAX_GRAPHEMES = 1000;
22
+
23
+
export const MAX_THREAD_DEPTH = 10;
+7
src/db/index.ts
+7
src/db/index.ts
+8
src/db/migrate.ts
+8
src/db/migrate.ts
···
1
+
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
2
+
import { drizzle } from "drizzle-orm/bun-sqlite";
3
+
import { Database } from "bun:sqlite";
4
+
import { env } from "../env";
5
+
6
+
const sqlite = new Database(env.DB_PATH);
7
+
const db = drizzle(sqlite);
8
+
migrate(db, { migrationsFolder: "./drizzle" });
+30
src/db/schema.ts
+30
src/db/schema.ts
···
1
+
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
2
+
import { sql } from "drizzle-orm";
3
+
4
+
export const conversations = sqliteTable("conversations", {
5
+
id: text().unique().notNull(),
6
+
did: text().notNull().unique(),
7
+
postUri: text("post_uri").notNull(),
8
+
revision: text().notNull(),
9
+
createdAt: integer("created_at", { mode: "timestamp" }).default(
10
+
sql`CURRENT_TIMESTAMP`,
11
+
)
12
+
.notNull(),
13
+
lastActive: integer("last_active", { mode: "timestamp" }).default(
14
+
sql`CURRENT_TIMESTAMP`,
15
+
)
16
+
.notNull(),
17
+
});
18
+
19
+
export const messages = sqliteTable("messages", {
20
+
id: integer().primaryKey({ autoIncrement: true }).notNull(),
21
+
conversationId: text("conversation_id").notNull().references(() =>
22
+
conversations.id
23
+
),
24
+
revision: text().notNull().references(() => conversations.revision),
25
+
did: text().notNull(),
26
+
postUri: text("post_uri").notNull(),
27
+
text: text().notNull(),
28
+
created_at: integer({ mode: "timestamp" }).default(sql`CURRENT_TIMESTAMP`)
29
+
.notNull(),
30
+
});
+24
src/env.ts
+24
src/env.ts
···
1
+
import { z } from "zod";
2
+
3
+
const envSchema = z.object({
4
+
AUTHORIZED_USERS: z.preprocess(
5
+
(val) =>
6
+
(typeof val === "string" && val.trim() !== "") ? val.split(",") : null,
7
+
z.array(z.string()).nullable().default(null),
8
+
),
9
+
10
+
SERVICE: z.string().default("https://bsky.social"),
11
+
DB_PATH: z.string().default("sqlite.db"),
12
+
GEMINI_MODEL: z.string().default("gemini-2.5-flash"),
13
+
14
+
ADMIN_DID: z.string(),
15
+
ADMIN_HANDLE: z.string(),
16
+
DID: z.string(),
17
+
HANDLE: z.string(),
18
+
BSKY_PASSWORD: z.string(),
19
+
20
+
GEMINI_API_KEY: z.string(),
21
+
});
22
+
23
+
export type Env = z.infer<typeof envSchema>;
24
+
export const env = envSchema.parse(Bun.env);
+163
src/handlers/messages.ts
+163
src/handlers/messages.ts
···
1
+
import modelPrompt from "../model/prompt.txt";
2
+
import { ChatMessage, Conversation } from "@skyware/bot";
3
+
import * as c from "../core";
4
+
import * as tools from "../tools";
5
+
import consola from "consola";
6
+
import { env } from "../env";
7
+
import {
8
+
exceedsGraphemes,
9
+
multipartResponse,
10
+
parseConversation,
11
+
saveMessage,
12
+
} from "../utils/conversation";
13
+
14
+
const logger = consola.withTag("Message Handler");
15
+
16
+
type SupportedFunctionCall = typeof c.SUPPORTED_FUNCTION_CALLS[number];
17
+
18
+
async function generateAIResponse(parsedConversation: string) {
19
+
const config = {
20
+
model: env.GEMINI_MODEL,
21
+
config: {
22
+
tools: tools.declarations,
23
+
},
24
+
};
25
+
26
+
const contents = [
27
+
{
28
+
role: "model" as const,
29
+
parts: [
30
+
{
31
+
text: modelPrompt
32
+
.replace("{{ handle }}", env.HANDLE),
33
+
},
34
+
],
35
+
},
36
+
{
37
+
role: "user" as const,
38
+
parts: [
39
+
{
40
+
text:
41
+
`Below is the yaml for the current conversation. The last message is the one to respond to. The post is the current one you are meant to be analyzing.
42
+
43
+
${parsedConversation}`,
44
+
},
45
+
],
46
+
},
47
+
];
48
+
49
+
let inference = await c.ai.models.generateContent({
50
+
...config,
51
+
contents,
52
+
});
53
+
54
+
logger.log(
55
+
`Initial inference took ${inference.usageMetadata?.totalTokenCount} tokens`,
56
+
);
57
+
58
+
if (inference.functionCalls && inference.functionCalls.length > 0) {
59
+
const call = inference.functionCalls[0];
60
+
61
+
if (
62
+
call &&
63
+
c.SUPPORTED_FUNCTION_CALLS.includes(
64
+
call.name as SupportedFunctionCall,
65
+
)
66
+
) {
67
+
logger.log("Function called invoked:", call.name);
68
+
69
+
const functionResponse = await tools.handler(
70
+
call as typeof call & { name: SupportedFunctionCall },
71
+
);
72
+
73
+
logger.log("Function response:", functionResponse);
74
+
75
+
//@ts-ignore
76
+
contents.push(inference.candidates[0]?.content!);
77
+
78
+
contents.push({
79
+
role: "user" as const,
80
+
parts: [{
81
+
//@ts-ignore
82
+
functionResponse: {
83
+
name: call.name as string,
84
+
response: { res: functionResponse },
85
+
},
86
+
}],
87
+
});
88
+
89
+
inference = await c.ai.models.generateContent({
90
+
...config,
91
+
contents,
92
+
});
93
+
}
94
+
}
95
+
96
+
return inference;
97
+
}
98
+
99
+
async function sendResponse(
100
+
conversation: Conversation,
101
+
text: string,
102
+
): Promise<void> {
103
+
if (exceedsGraphemes(text)) {
104
+
multipartResponse(conversation, text);
105
+
} else {
106
+
conversation.sendMessage({
107
+
text,
108
+
});
109
+
}
110
+
}
111
+
112
+
export async function handler(message: ChatMessage): Promise<void> {
113
+
const conversation = await message.getConversation();
114
+
// ? Conversation should always be able to be found, but just in case:
115
+
if (!conversation) {
116
+
logger.error("Cannot find conversation");
117
+
return;
118
+
}
119
+
120
+
const authorized = env.AUTHORIZED_USERS == null
121
+
? true
122
+
: env.AUTHORIZED_USERS.includes(message.senderDid as any);
123
+
124
+
if (!authorized) {
125
+
conversation.sendMessage({
126
+
text: c.UNAUTHORIZED_MESSAGE,
127
+
});
128
+
129
+
return;
130
+
}
131
+
132
+
logger.success("Found conversation");
133
+
conversation.sendMessage({
134
+
text: "...",
135
+
});
136
+
137
+
const parsedConversation = await parseConversation(conversation);
138
+
139
+
logger.info("Parsed conversation: ", parsedConversation);
140
+
141
+
try {
142
+
const inference = await generateAIResponse(parsedConversation);
143
+
if (!inference) {
144
+
throw new Error("Failed to generate text. Returned undefined.");
145
+
}
146
+
147
+
logger.success("Generated text:", inference.text);
148
+
149
+
saveMessage(conversation, env.DID, inference.text!);
150
+
151
+
const responseText = inference.text;
152
+
if (responseText) {
153
+
await sendResponse(conversation, responseText);
154
+
}
155
+
} catch (error) {
156
+
logger.error("Error in post handler:", error);
157
+
158
+
await conversation.sendMessage({
159
+
text:
160
+
"Sorry, I ran into an issue analyzing that post. Please try again.",
161
+
});
162
+
}
163
+
}
+26
src/index.ts
+26
src/index.ts
···
1
+
import * as messages from "./handlers/messages";
2
+
import { env } from "./env";
3
+
import { bot } from "./core";
4
+
import consola from "consola";
5
+
import { IncomingChatPreference } from "@skyware/bot";
6
+
7
+
const logger = consola.withTag("Entrypoint");
8
+
9
+
logger.info("Logging in..");
10
+
11
+
try {
12
+
await bot.login({
13
+
identifier: env.HANDLE,
14
+
password: env.BSKY_PASSWORD,
15
+
});
16
+
17
+
logger.success(`Logged in as @${env.HANDLE} (${env.DID})`);
18
+
19
+
await bot.setChatPreference(IncomingChatPreference.All);
20
+
bot.on("message", messages.handler);
21
+
22
+
logger.success("Registered events (reply, mention, quote)");
23
+
} catch (e) {
24
+
logger.error("Failure to log-in: ", e);
25
+
process.exit(1);
26
+
}
+14
src/model/prompt.txt
+14
src/model/prompt.txt
···
1
+
You are Aero, a neutral and helpful assistant on Bluesky.
2
+
Your job is to give clear, factual, and concise explanations or context about posts users send you.
3
+
4
+
Handle: {{ handle }}
5
+
6
+
Guidelines:
7
+
8
+
* Always stay neutral and avoid opinions or bias.
9
+
* Give short, factual background or definitions that help users understand a post.
10
+
* If something is unclear, briefly explain possible meanings.
11
+
* Keep every reply as concise as possible while staying complete.
12
+
* Never exceed 1000 graphemes.
13
+
* Do not speculate or include unverified information; say if something is uncertain.
14
+
* Write in plain text only. Do not use markdown, symbols, or formatting.
+31
src/tools/index.ts
+31
src/tools/index.ts
···
1
+
import type { FunctionCall } from "@google/genai";
2
+
import * as search_posts from "./search_posts";
3
+
import type { infer as z_infer } from "zod";
4
+
5
+
const validation_mappings = {
6
+
"search_posts": search_posts.validator,
7
+
} as const;
8
+
9
+
export const declarations = [
10
+
{ urlContext: {} },
11
+
{ googleSearch: {} },
12
+
/*
13
+
{
14
+
functionDeclarations: [
15
+
search_posts.definition,
16
+
],
17
+
},
18
+
*/
19
+
];
20
+
21
+
type ToolName = keyof typeof validation_mappings;
22
+
export async function handler(call: FunctionCall & { name: ToolName }) {
23
+
const parsedArgs = validation_mappings[call.name].parse(call.args);
24
+
25
+
switch (call.name) {
26
+
case "search_posts":
27
+
return await search_posts.handler(
28
+
parsedArgs as z_infer<typeof search_posts.validator>,
29
+
);
30
+
}
31
+
}
+26
src/tools/search_posts.ts
+26
src/tools/search_posts.ts
···
1
+
import { AtUri } from "@atproto/syntax";
2
+
import { ai, bot } from "../core";
3
+
import { Type } from "@google/genai";
4
+
import { env } from "../env";
5
+
import z from "zod";
6
+
7
+
export const definition = {
8
+
name: "search_posts",
9
+
description: "Searches posts across the entire Bluesky network.",
10
+
parameters: {
11
+
type: Type.OBJECT,
12
+
properties: {
13
+
query: {
14
+
type: Type.STRING,
15
+
description: "The query to search for.",
16
+
},
17
+
},
18
+
required: ["query"],
19
+
},
20
+
};
21
+
22
+
export const validator = z.object({
23
+
query: z.string(),
24
+
});
25
+
26
+
export async function handler(args: z.infer<typeof validator>) {}
+233
src/utils/conversation.ts
+233
src/utils/conversation.ts
···
1
+
import {
2
+
type ChatMessage,
3
+
type Conversation,
4
+
graphemeLength,
5
+
} from "@skyware/bot";
6
+
import * as yaml from "js-yaml";
7
+
import db from "../db";
8
+
import { conversations, messages } from "../db/schema";
9
+
import { and, eq } from "drizzle-orm";
10
+
import { env } from "../env";
11
+
import { bot, MAX_GRAPHEMES } from "../core";
12
+
import { traverseThread } from "./thread";
13
+
14
+
const resolveDid = (convo: Conversation, did: string) =>
15
+
convo.members.find((actor) => actor.did == did)!;
16
+
17
+
const getUserDid = (convo: Conversation) =>
18
+
convo.members.find((actor) => actor.did != env.DID)!;
19
+
20
+
function generateRevision(bytes = 8) {
21
+
const array = new Uint8Array(bytes);
22
+
crypto.getRandomValues(array);
23
+
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
24
+
}
25
+
26
+
async function initConvo(convo: Conversation) {
27
+
const user = getUserDid(convo);
28
+
29
+
const initialMessage = (await convo.getMessages()).messages[0] as
30
+
| ChatMessage
31
+
| undefined;
32
+
if (!initialMessage) {
33
+
throw new Error("Failed to get initial message of conversation");
34
+
}
35
+
36
+
const postUri = await parseMessagePostUri(initialMessage);
37
+
if (!postUri) {
38
+
convo.sendMessage({
39
+
text:
40
+
"Please send a post for me to make sense of the noise for you.",
41
+
});
42
+
throw new Error("No post reference in initial message.");
43
+
}
44
+
45
+
return await db.transaction(async (tx) => {
46
+
const [_convo] = await tx
47
+
.insert(conversations)
48
+
.values({
49
+
id: convo.id,
50
+
did: user.did,
51
+
postUri,
52
+
revision: generateRevision(),
53
+
})
54
+
.returning();
55
+
56
+
if (!_convo) {
57
+
throw new Error("Error during database transaction");
58
+
}
59
+
60
+
await tx
61
+
.insert(messages)
62
+
.values({
63
+
conversationId: _convo.id,
64
+
did: user.did,
65
+
postUri,
66
+
revision: _convo.revision,
67
+
text: initialMessage.text,
68
+
});
69
+
70
+
return _convo!;
71
+
});
72
+
}
73
+
74
+
async function getConvo(convoId: string) {
75
+
const [convo] = await db
76
+
.select()
77
+
.from(conversations)
78
+
.where(eq(conversations.id, convoId))
79
+
.limit(1);
80
+
81
+
return convo;
82
+
}
83
+
84
+
export async function parseConversation(convo: Conversation) {
85
+
let row = await getConvo(convo.id);
86
+
if (!row) {
87
+
row = await initConvo(convo);
88
+
} else {
89
+
const latestMessage = (await convo.getMessages())
90
+
.messages[0] as ChatMessage;
91
+
92
+
const postUri = await parseMessagePostUri(latestMessage);
93
+
if (postUri) {
94
+
const [updatedRow] = await db
95
+
.update(conversations)
96
+
.set({
97
+
postUri,
98
+
revision: generateRevision(),
99
+
})
100
+
.returning();
101
+
102
+
if (!updatedRow) {
103
+
throw new Error("Failed to update conversation in database");
104
+
}
105
+
106
+
row = updatedRow;
107
+
}
108
+
109
+
await db
110
+
.insert(messages)
111
+
.values({
112
+
conversationId: convo.id,
113
+
did: getUserDid(convo).did,
114
+
postUri: row.postUri,
115
+
revision: row.revision,
116
+
text: latestMessage!.text,
117
+
});
118
+
}
119
+
120
+
const post = await bot.getPost(row.postUri);
121
+
const convoMessages = await getRelevantMessages(row!);
122
+
123
+
const thread = await traverseThread(post);
124
+
125
+
return yaml.dump({
126
+
post: {
127
+
thread: {
128
+
ancestors: thread.map((post) => ({
129
+
author: post.author.displayName
130
+
? `${post.author.displayName} (${post.author.handle})`
131
+
: `Handle: ${post.author.handle}`,
132
+
text: post.text,
133
+
})),
134
+
},
135
+
author: post.author.displayName
136
+
? `${post.author.displayName} (${post.author.handle})`
137
+
: `Handle: ${post.author.handle}`,
138
+
text: post.text,
139
+
likes: post.likeCount || 0,
140
+
replies: post.replyCount || 0,
141
+
},
142
+
messages: convoMessages.map((message) => {
143
+
const profile = resolveDid(convo, message.did);
144
+
145
+
return {
146
+
user: profile.displayName
147
+
? `${profile.displayName} (${profile.handle})`
148
+
: `Handle: ${profile.handle}`,
149
+
text: message.text,
150
+
};
151
+
}),
152
+
});
153
+
}
154
+
155
+
async function parseMessagePostUri(message: ChatMessage) {
156
+
if (!message.embed) return null;
157
+
const post = message.embed;
158
+
return post.uri;
159
+
}
160
+
161
+
async function getRelevantMessages(convo: typeof conversations.$inferSelect) {
162
+
const convoMessages = await db
163
+
.select()
164
+
.from(messages)
165
+
.where(
166
+
and(
167
+
eq(messages.conversationId, convo.id),
168
+
eq(messages.postUri, convo!.postUri),
169
+
),
170
+
)
171
+
.limit(15);
172
+
173
+
return convoMessages;
174
+
}
175
+
176
+
export async function saveMessage(
177
+
convo: Conversation,
178
+
did: string,
179
+
text: string,
180
+
) {
181
+
const _convo = await getConvo(convo.id);
182
+
if (!_convo) {
183
+
throw new Error("Failed to find conversation with ID: " + convo.id);
184
+
}
185
+
186
+
await db
187
+
.insert(messages)
188
+
.values({
189
+
conversationId: _convo.id,
190
+
postUri: _convo.postUri,
191
+
revision: _convo.postUri,
192
+
did,
193
+
text,
194
+
});
195
+
}
196
+
197
+
export function exceedsGraphemes(content: string) {
198
+
return graphemeLength(content) > MAX_GRAPHEMES;
199
+
}
200
+
201
+
export function splitResponse(text: string): string[] {
202
+
const words = text.split(" ");
203
+
const chunks: string[] = [];
204
+
let currentChunk = "";
205
+
206
+
for (const word of words) {
207
+
if (currentChunk.length + word.length + 1 < MAX_GRAPHEMES - 10) {
208
+
currentChunk += ` ${word}`;
209
+
} else {
210
+
chunks.push(currentChunk.trim());
211
+
currentChunk = word;
212
+
}
213
+
}
214
+
215
+
if (currentChunk.trim()) {
216
+
chunks.push(currentChunk.trim());
217
+
}
218
+
219
+
const total = chunks.length;
220
+
if (total <= 1) return [text];
221
+
222
+
return chunks.map((chunk, i) => `(${i + 1}/${total}) ${chunk}`);
223
+
}
224
+
225
+
export async function multipartResponse(convo: Conversation, content: string) {
226
+
const parts = splitResponse(content).filter((p) => p.trim().length > 0);
227
+
228
+
for (const segment of parts) {
229
+
await convo.sendMessage({
230
+
text: segment,
231
+
});
232
+
}
233
+
}
+40
src/utils/thread.ts
+40
src/utils/thread.ts
···
1
+
import { Post } from "@skyware/bot";
2
+
import * as c from "../core";
3
+
import * as yaml from "js-yaml";
4
+
5
+
/*
6
+
Traversal
7
+
*/
8
+
export async function traverseThread(post: Post): Promise<Post[]> {
9
+
const thread: Post[] = [
10
+
post,
11
+
];
12
+
let currentPost: Post | undefined = post;
13
+
let parentCount = 0;
14
+
15
+
while (
16
+
currentPost && parentCount < c.MAX_THREAD_DEPTH
17
+
) {
18
+
const parentPost = await currentPost.fetchParent();
19
+
20
+
if (parentPost) {
21
+
thread.push(parentPost);
22
+
currentPost = parentPost;
23
+
} else {
24
+
break;
25
+
}
26
+
parentCount++;
27
+
}
28
+
29
+
return thread.reverse();
30
+
}
31
+
32
+
export function parseThread(thread: Post[]) {
33
+
return yaml.dump({
34
+
uri: thread[0]!.uri,
35
+
posts: thread.map((post) => ({
36
+
author: `${post.author.displayName} (${post.author.handle})`,
37
+
text: post.text,
38
+
})),
39
+
});
40
+
}
+29
tsconfig.json
+29
tsconfig.json
···
1
+
{
2
+
"compilerOptions": {
3
+
// Environment setup & latest features
4
+
"lib": ["ESNext"],
5
+
"target": "ESNext",
6
+
"module": "Preserve",
7
+
"moduleDetection": "force",
8
+
"jsx": "react-jsx",
9
+
"allowJs": true,
10
+
11
+
// Bundler mode
12
+
"moduleResolution": "bundler",
13
+
"allowImportingTsExtensions": true,
14
+
"verbatimModuleSyntax": true,
15
+
"noEmit": true,
16
+
17
+
// Best practices
18
+
"strict": true,
19
+
"skipLibCheck": true,
20
+
"noFallthroughCasesInSwitch": true,
21
+
"noUncheckedIndexedAccess": true,
22
+
"noImplicitOverride": true,
23
+
24
+
// Some stricter flags (disabled by default)
25
+
"noUnusedLocals": false,
26
+
"noUnusedParameters": false,
27
+
"noPropertyAccessFromIndexSignature": false
28
+
}
29
+
}