A simple Bluesky bot to make sense of the noise, with responses powered by Gemini, similar to Grok.

init

indexx.dev 491808ab

+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
··· 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
··· 1 + FROM oven/bun:latest 2 + 3 + WORKDIR /app 4 + 5 + COPY package.json bun.lock ./ 6 + 7 + RUN bun install --frozen-lockfile 8 + 9 + COPY . . 10 + 11 + CMD ["bun", "start"]
+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
··· 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
··· 1 + import { defineConfig } from "drizzle-kit"; 2 + 3 + export default defineConfig({ 4 + dialect: "sqlite", 5 + schema: "./src/db/schema.ts", 6 + out: "./drizzle", 7 + });
+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
··· 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
··· 1 + { 2 + "version": "7", 3 + "dialect": "sqlite", 4 + "entries": [ 5 + { 6 + "idx": 0, 7 + "version": "6", 8 + "when": 1761119054619, 9 + "tag": "0000_black_micromax", 10 + "breakpoints": true 11 + } 12 + ] 13 + }
+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
··· 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
··· 1 + import { drizzle } from "drizzle-orm/bun-sqlite"; 2 + import { Database } from "bun:sqlite"; 3 + import * as schema from "./schema"; 4 + import { env } from "../env"; 5 + 6 + const sqlite = new Database(env.DB_PATH); 7 + export default drizzle(sqlite, { schema });
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 + }