init
This commit is contained in:
commit
491808abdc
23 changed files with 1260 additions and 0 deletions
20
.env.example
Normal file
20
.env.example
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Comma-separated list of users who can use the bot (delete var if you want everyone to be able to use it)
|
||||
AUTHORIZED_USERS=""
|
||||
|
||||
# PDS service URL (optional)
|
||||
SERVICE="https://bsky.social"
|
||||
|
||||
DB_PATH="data/sqlite.db"
|
||||
GEMINI_MODEL="gemini-2.5-flash"
|
||||
|
||||
ADMIN_DID=""
|
||||
ADMIN_HANDLE=""
|
||||
|
||||
DID=""
|
||||
HANDLE=""
|
||||
|
||||
# https://bsky.app/settings/app-passwords
|
||||
BSKY_PASSWORD=""
|
||||
|
||||
# https://aistudio.google.com/apikey
|
||||
GEMINI_API_KEY=""
|
||||
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
# Database
|
||||
data
|
||||
11
Dockerfile
Normal file
11
Dockerfile
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
FROM oven/bun:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lock ./
|
||||
|
||||
RUN bun install --frozen-lockfile
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["bun", "start"]
|
||||
16
README.md
Normal file
16
README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Aero
|
||||
|
||||
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.
|
||||
|
||||
## How to Use
|
||||
|
||||
- Find a post you want to ask about
|
||||
- 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.
|
||||
..after a few seconds, you'll get a response to your query!
|
||||
|
||||
**For any further queries:**
|
||||
|
||||
- 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.
|
||||
- You can ask up to 15 queries per post link, and all messages prior to the new post link are ignored.
|
||||
|
||||
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
Normal file
270
bun.lock
Normal file
|
|
@ -0,0 +1,270 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "bsky-echo",
|
||||
"dependencies": {
|
||||
"@atproto/syntax": "^0.4.0",
|
||||
"@google/genai": "^1.11.0",
|
||||
"@skyware/bot": "^0.3.12",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"consola": "^3.4.2",
|
||||
"drizzle-orm": "^0.44.4",
|
||||
"js-yaml": "^4.1.0",
|
||||
"zod": "^4.0.14",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.19",
|
||||
"drizzle-kit": "^0.31.4",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.8.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@atcute/atproto": ["@atcute/atproto@3.1.1", "", { "dependencies": { "@atcute/lexicons": "^1.1.0" } }, "sha512-D+RLTIPF0xLu7BPZY8KSewAPemJFh+3n3zeQ3ROsLxbTtCHbrTDMAmAFexaVRAPGcPYrwXaBUlv7yZjScJolMg=="],
|
||||
|
||||
"@atcute/bluesky": ["@atcute/bluesky@1.0.15", "", { "peerDependencies": { "@atcute/client": "^1.0.0 || ^2.0.0" } }, "sha512-+EFiybmKQ97aBAgtaD+cKRJER5AMn3cZMkEwEg/pDdWyzxYJ9m1UgemmLdTgI8VrxPufKqdXS2nl7uO7TY6BPA=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@atcute/cbor": ["@atcute/cbor@1.0.7", "", { "dependencies": { "@atcute/cid": "^1.0.3", "@atcute/multibase": "^1.0.0" } }, "sha512-z3chucgCqjAN36ySvUVl1VSwtGME4CDS173eaaEfiTSpRIQ6ewKpKlkzapLUNqtLU9iBx884b9c2j6kjEyn1XA=="],
|
||||
|
||||
"@atcute/cid": ["@atcute/cid@1.0.3", "", { "dependencies": { "@atcute/multibase": "^1.0.0", "@atcute/varint": "^1.0.1" } }, "sha512-BZbs+Xt0yMci0I2dLqqYsN76ua8lkMk/HQfEIKr7g2XMBlSc0XNCXfZdbAWPwiCK/NuGaPBocYMRwApd4dF2Qg=="],
|
||||
|
||||
"@atcute/client": ["@atcute/client@2.0.9", "", {}, "sha512-QNDm9gMP6x9LY77ArwY+urQOBtQW74/onEAz42c40JxRm6Rl9K9cU4ROvNKJ+5cpVmEm1sthEWVRmDr5CSZENA=="],
|
||||
|
||||
"@atcute/lexicons": ["@atcute/lexicons@1.1.0", "", { "dependencies": { "esm-env": "^1.2.2" } }, "sha512-LFqwnria78xLYb62Ri/+WwQpUTgZp2DuyolNGIIOV1dpiKhFFFh//nscHMA6IExFLQRqWDs3tTjy7zv0h3sf1Q=="],
|
||||
|
||||
"@atcute/multibase": ["@atcute/multibase@1.1.4", "", { "dependencies": { "@atcute/uint8array": "^1.0.2" } }, "sha512-NUf5AeeSOmuZHGU+4GAaMtISJoG+ZHtW/vUVA4lK/YDt/7LODAW0Fd0NNIIUPVUoW0xJS6zSEIWvwLLuxmEHhA=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@atcute/uint8array": ["@atcute/uint8array@1.0.3", "", {}, "sha512-M/K+ihiVW8Pl2PFLzaC4E3l4JaZ1IH05Q0AbPWUC4cVHnd/gZ/1kAF5ngdtGvJeDMirHZ2VAy7OmAsPwR/2nlA=="],
|
||||
|
||||
"@atcute/varint": ["@atcute/varint@1.0.2", "", {}, "sha512-0O31hePzzr4O3NGWHUKKOyta6CGSL+AtN8iir8grGxu9jXyI7DBARlw6PbgKA6uTAvsXdpmRmF8MX+p0TsLnNg=="],
|
||||
|
||||
"@atproto/syntax": ["@atproto/syntax@0.4.0", "", {}, "sha512-b9y5ceHS8YKOfP3mdKmwAx5yVj9294UN7FG2XzP6V5aKUdFazEYRnR9m5n5ZQFKa3GNvz7de9guZCJ/sUTcOAA=="],
|
||||
|
||||
"@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.8", "", { "os": "aix", "cpu": "ppc64" }, "sha512-urAvrUedIqEiFR3FYSLTWQgLu5tb+m0qZw0NBEasUeo6wuqatkMDaRT+1uABiGXEu5vqgPd7FGE1BhsAIy9QVA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.8", "", { "os": "android", "cpu": "arm" }, "sha512-RONsAvGCz5oWyePVnLdZY/HHwA++nxYWIX1atInlaW6SEkwq6XkP3+cb825EUcRs5Vss/lGh/2YxAb5xqc07Uw=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.8", "", { "os": "android", "cpu": "arm64" }, "sha512-OD3p7LYzWpLhZEyATcTSJ67qB5D+20vbtr6vHlHWSQYhKtzUYrETuWThmzFpZtFsBIxRvhO07+UgVA9m0i/O1w=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.8", "", { "os": "android", "cpu": "x64" }, "sha512-yJAVPklM5+4+9dTeKwHOaA+LQkmrKFX96BM0A/2zQrbS6ENCmxc4OVoBs5dPkCCak2roAD+jKCdnmOqKszPkjA=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Jw0mxgIaYX6R8ODrdkLLPwBqHTtYHJSmzzd+QeytSugzQ0Vg4c5rDky5VgkoowbZQahCbsv1rT1KW72MPIkevw=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Vh2gLxxHnuoQ+GjPNvDSDRpoBCUzY4Pu0kBqMBDlK4fuWbKgGtmDIeEC081xi26PPjn+1tct+Bh8FjyLlw1Zlg=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.8", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-YPJ7hDQ9DnNe5vxOm6jaie9QsTwcKedPvizTVlqWG9GBSq+BuyWEDazlGaDTC5NGU4QJd666V0yqCBL2oWKPfA=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-MmaEXxQRdXNFsRN/KcIimLnSJrk2r5H8v+WVafRWz5xdSVmWLoITZQXcgehI2ZE6gioE6HirAEToM/RvFBeuhw=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.8", "", { "os": "linux", "cpu": "arm" }, "sha512-FuzEP9BixzZohl1kLf76KEVOsxtIBFwCaLupVuk4eFVnOZfU+Wsn+x5Ryam7nILV2pkq2TqQM9EZPsOBuMC+kg=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-WIgg00ARWv/uYLU7lsuDK00d/hHSfES5BzdWAdAig1ioV5kaFNrtK8EqGcUBJhYqotlUByUKz5Qo6u8tt7iD/w=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.8", "", { "os": "linux", "cpu": "ia32" }, "sha512-A1D9YzRX1i+1AJZuFFUMP1E9fMaYY+GnSQil9Tlw05utlE86EKTUA7RjwHDkEitmLYiFsRd9HwKBPEftNdBfjg=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.8", "", { "os": "linux", "cpu": "none" }, "sha512-O7k1J/dwHkY1RMVvglFHl1HzutGEFFZ3kNiDMSOyUrB7WcoHGf96Sh+64nTRT26l3GMbCW01Ekh/ThKM5iI7hQ=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.8", "", { "os": "linux", "cpu": "none" }, "sha512-uv+dqfRazte3BzfMp8PAQXmdGHQt2oC/y2ovwpTteqrMx2lwaksiFZ/bdkXJC19ttTvNXBuWH53zy/aTj1FgGw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-GyG0KcMi1GBavP5JgAkkstMGyMholMDybAf8wF5A70CALlDM2p/f7YFE7H92eDeH/VBtFJA5MT4nRPDGg4JuzQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.8", "", { "os": "linux", "cpu": "none" }, "sha512-rAqDYFv3yzMrq7GIcen3XP7TUEG/4LK86LUPMIz6RT8A6pRIDn0sDcvjudVZBiiTcZCY9y2SgYX2lgK3AF+1eg=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-Xutvh6VjlbcHpsIIbwY8GVRbwoviWT19tFhgdA7DlenLGC/mbc3lBoVb7jxj9Z+eyGqvcnSyIltYUrkKzWqSvg=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.8", "", { "os": "linux", "cpu": "x64" }, "sha512-ASFQhgY4ElXh3nDcOMTkQero4b1lgubskNlhIfJrsH5OKZXDpUAKBlNS0Kx81jwOBp+HCeZqmoJuihTv57/jvQ=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.8", "", { "os": "none", "cpu": "arm64" }, "sha512-d1KfruIeohqAi6SA+gENMuObDbEjn22olAR7egqnkCD9DGBG0wsEARotkLgXDu6c4ncgWTZJtN5vcgxzWRMzcw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.8", "", { "os": "none", "cpu": "x64" }, "sha512-nVDCkrvx2ua+XQNyfrujIG38+YGyuy2Ru9kKVNyh5jAys6n+l44tTtToqHjino2My8VAY6Lw9H7RI73XFi66Cg=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.8", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-j8HgrDuSJFAujkivSMSfPQSAa5Fxbvk4rgNAS5i3K+r8s1X0p1uOO2Hl2xNsGFppOeHOLAVgYwDVlmxhq5h+SQ=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.8", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1h8MUAwa0VhNCDp6Af0HToI2TJFAn1uqT9Al6DJVzdIBAd21m/G0Yfc77KDM3uF3T/YaOgQq3qTJHPbTOInaIQ=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.8", "", { "os": "none", "cpu": "arm64" }, "sha512-r2nVa5SIK9tSWd0kJd9HCffnDHKchTGikb//9c7HX+r+wHYCpQrSgxhlY6KWV1nFo1l4KFbsMlHk+L6fekLsUg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.8", "", { "os": "sunos", "cpu": "x64" }, "sha512-zUlaP2S12YhQ2UzUfcCuMDHQFJyKABkAjvO5YSndMiIkMimPmxA+BYSBikWgsRpvyxuRnow4nS5NPnf9fpv41w=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-YEGFFWESlPva8hGL+zvj2z/SaK+pH0SwOM0Nc/d+rVnW7GSTFlLBGzZkuSU9kFIGIo8q9X3ucpZhu8PDN5A2sQ=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.8", "", { "os": "win32", "cpu": "ia32" }, "sha512-hiGgGC6KZ5LZz58OL/+qVVoZiuZlUYlYHNAmczOm7bs2oE1XriPFi5ZHHrS8ACpV5EjySrnoCKmcbQMN+ojnHg=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.8", "", { "os": "win32", "cpu": "x64" }, "sha512-cn3Yr7+OaaZq1c+2pe+8yxC8E144SReCQjN6/2ynubzYjvyqZjTXfQJpAcQpsdJq3My7XADANiYGHoFC69pLQw=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
|
||||
|
||||
"@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
|
||||
|
||||
"@types/node": ["@types/node@24.0.15", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-oaeTSbCef7U/z7rDeJA138xpG3NuKc64/rZ2qmUFkFJmnMsAPaluIifqyWd8hSSMxyP9oie3dLAqYPblag9KgA=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
|
||||
|
||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||
|
||||
"buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
|
||||
|
||||
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
||||
|
||||
"esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="],
|
||||
|
||||
"event-target-polyfill": ["event-target-polyfill@0.0.4", "", {}, "sha512-Gs6RLjzlLRdT8X9ZipJdIZI/Y6/HhRLyq9RdDlCsnpxr/+Nn6bU2EFGuC94GjxqhM+Nmij2Vcq98yoHrU8uNFQ=="],
|
||||
|
||||
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"google-logging-utils": ["google-logging-utils@0.0.2", "", {}, "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ=="],
|
||||
|
||||
"gtoken": ["gtoken@7.1.0", "", { "dependencies": { "gaxios": "^6.0.0", "jws": "^4.0.0" } }, "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||
|
||||
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"jws": ["jws@4.0.0", "", { "dependencies": { "jwa": "^2.0.0", "safe-buffer": "^5.0.1" } }, "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"partysocket": ["partysocket@1.1.4", "", { "dependencies": { "event-target-polyfill": "^0.0.4" } }, "sha512-jXP7PFj2h5/v4UjDS8P7MZy6NJUQ7sspiFyxL4uc/+oKOL+KdtXzHnTV8INPGxBrLTXgalyG3kd12Qm7WrYc3A=="],
|
||||
|
||||
"quick-lru": ["quick-lru@7.0.1", "", {}, "sha512-kLjThirJMkWKutUKbZ8ViqFc09tDQhlbQo2MNuVeLWbRauqYP96Sm6nzlQ24F0HFjUNZ4i9+AgldJ9H6DZXi7g=="],
|
||||
|
||||
"rate-limit-threshold": ["rate-limit-threshold@0.1.5", "", {}, "sha512-75vpvXC/ZqQJrFDp0dVtfoXZi8kxQP2eBuxVYFvGDfnHhcgE+ZG870u4ItQhWQh54Y6nNwOaaq5g3AL9n27lTg=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"tiny-emitter": ["tiny-emitter@2.1.0", "", {}, "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
|
||||
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"zod": ["zod@4.0.14", "", {}, "sha512-nGFJTnJN6cM2v9kXL+SOBq3AtjQby3Mv5ySGFof5UGRHrRioSJ5iG680cYNjE/yWk671nROcpPj4hAS8nyLhSw=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="],
|
||||
|
||||
"@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="],
|
||||
}
|
||||
}
|
||||
7
drizzle.config.ts
Normal file
7
drizzle.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { defineConfig } from "drizzle-kit";
|
||||
|
||||
export default defineConfig({
|
||||
dialect: "sqlite",
|
||||
schema: "./src/db/schema.ts",
|
||||
out: "./drizzle",
|
||||
});
|
||||
22
drizzle/0000_black_micromax.sql
Normal file
22
drizzle/0000_black_micromax.sql
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
CREATE TABLE `conversations` (
|
||||
`id` text NOT NULL,
|
||||
`did` text NOT NULL,
|
||||
`post_uri` text NOT NULL,
|
||||
`revision` text NOT NULL,
|
||||
`created_at` integer DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
`last_active` integer DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `conversations_id_unique` ON `conversations` (`id`);--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `conversations_did_unique` ON `conversations` (`did`);--> statement-breakpoint
|
||||
CREATE TABLE `messages` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`conversation_id` text NOT NULL,
|
||||
`revision` text NOT NULL,
|
||||
`did` text NOT NULL,
|
||||
`post_uri` text NOT NULL,
|
||||
`text` text NOT NULL,
|
||||
`created_at` integer DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
FOREIGN KEY (`conversation_id`) REFERENCES `conversations`(`id`) ON UPDATE no action ON DELETE no action,
|
||||
FOREIGN KEY (`revision`) REFERENCES `conversations`(`revision`) ON UPDATE no action ON DELETE no action
|
||||
);
|
||||
174
drizzle/meta/0000_snapshot.json
Normal file
174
drizzle/meta/0000_snapshot.json
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "77441a9c-7dba-4cc4-b9db-5720373932d8",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"conversations": {
|
||||
"name": "conversations",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"did": {
|
||||
"name": "did",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"post_uri": {
|
||||
"name": "post_uri",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"revision": {
|
||||
"name": "revision",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
},
|
||||
"last_active": {
|
||||
"name": "last_active",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"conversations_id_unique": {
|
||||
"name": "conversations_id_unique",
|
||||
"columns": [
|
||||
"id"
|
||||
],
|
||||
"isUnique": true
|
||||
},
|
||||
"conversations_did_unique": {
|
||||
"name": "conversations_did_unique",
|
||||
"columns": [
|
||||
"did"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"messages": {
|
||||
"name": "messages",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"conversation_id": {
|
||||
"name": "conversation_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"revision": {
|
||||
"name": "revision",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"did": {
|
||||
"name": "did",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"post_uri": {
|
||||
"name": "post_uri",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"text": {
|
||||
"name": "text",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "CURRENT_TIMESTAMP"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"messages_conversation_id_conversations_id_fk": {
|
||||
"name": "messages_conversation_id_conversations_id_fk",
|
||||
"tableFrom": "messages",
|
||||
"tableTo": "conversations",
|
||||
"columnsFrom": [
|
||||
"conversation_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
"messages_revision_conversations_revision_fk": {
|
||||
"name": "messages_revision_conversations_revision_fk",
|
||||
"tableFrom": "messages",
|
||||
"tableTo": "conversations",
|
||||
"columnsFrom": [
|
||||
"revision"
|
||||
],
|
||||
"columnsTo": [
|
||||
"revision"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
13
drizzle/meta/_journal.json
Normal file
13
drizzle/meta/_journal.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"version": "7",
|
||||
"dialect": "sqlite",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1761119054619,
|
||||
"tag": "0000_black_micromax",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
36
package.json
Normal file
36
package.json
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{
|
||||
"name": "bsky-aero",
|
||||
"description": "A simple Bluesky bot to make sense of all the noise, with responses powered by Gemini.",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "bun run src/index.ts",
|
||||
"dev": "bun run --watch src/index.ts",
|
||||
"db:generate": "bunx drizzle-kit generate --dialect sqlite --schema ./src/db/schema.ts",
|
||||
"db:migrate": "bun run src/db/migrate.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.2.19",
|
||||
"drizzle-kit": "^0.31.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/syntax": "^0.4.0",
|
||||
"@google/genai": "^1.11.0",
|
||||
"@skyware/bot": "^0.3.12",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"consola": "^3.4.2",
|
||||
"drizzle-orm": "^0.44.4",
|
||||
"js-yaml": "^4.1.0",
|
||||
"zod": "^4.0.14"
|
||||
},
|
||||
"repository": {
|
||||
"url": "https://github.com/indexxing/echo"
|
||||
},
|
||||
"author": {
|
||||
"name": "Index",
|
||||
"email": "contact@indexx.dev"
|
||||
}
|
||||
}
|
||||
23
src/core.ts
Normal file
23
src/core.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { GoogleGenAI } from "@google/genai";
|
||||
import { Bot } from "@skyware/bot";
|
||||
import { env } from "./env";
|
||||
|
||||
export const bot = new Bot({
|
||||
service: env.SERVICE,
|
||||
emitChatEvents: true,
|
||||
});
|
||||
|
||||
export const ai = new GoogleGenAI({
|
||||
apiKey: env.GEMINI_API_KEY,
|
||||
});
|
||||
|
||||
export const UNAUTHORIZED_MESSAGE =
|
||||
"I can’t make sense of your noise just yet. You’ll need to be whitelisted before I can help.";
|
||||
|
||||
export const SUPPORTED_FUNCTION_CALLS = [
|
||||
"search_posts",
|
||||
] as const;
|
||||
|
||||
export const MAX_GRAPHEMES = 1000;
|
||||
|
||||
export const MAX_THREAD_DEPTH = 10;
|
||||
7
src/db/index.ts
Normal file
7
src/db/index.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import { Database } from "bun:sqlite";
|
||||
import * as schema from "./schema";
|
||||
import { env } from "../env";
|
||||
|
||||
const sqlite = new Database(env.DB_PATH);
|
||||
export default drizzle(sqlite, { schema });
|
||||
8
src/db/migrate.ts
Normal file
8
src/db/migrate.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||
import { Database } from "bun:sqlite";
|
||||
import { env } from "../env";
|
||||
|
||||
const sqlite = new Database(env.DB_PATH);
|
||||
const db = drizzle(sqlite);
|
||||
migrate(db, { migrationsFolder: "./drizzle" });
|
||||
30
src/db/schema.ts
Normal file
30
src/db/schema.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
import { sql } from "drizzle-orm";
|
||||
|
||||
export const conversations = sqliteTable("conversations", {
|
||||
id: text().unique().notNull(),
|
||||
did: text().notNull().unique(),
|
||||
postUri: text("post_uri").notNull(),
|
||||
revision: text().notNull(),
|
||||
createdAt: integer("created_at", { mode: "timestamp" }).default(
|
||||
sql`CURRENT_TIMESTAMP`,
|
||||
)
|
||||
.notNull(),
|
||||
lastActive: integer("last_active", { mode: "timestamp" }).default(
|
||||
sql`CURRENT_TIMESTAMP`,
|
||||
)
|
||||
.notNull(),
|
||||
});
|
||||
|
||||
export const messages = sqliteTable("messages", {
|
||||
id: integer().primaryKey({ autoIncrement: true }).notNull(),
|
||||
conversationId: text("conversation_id").notNull().references(() =>
|
||||
conversations.id
|
||||
),
|
||||
revision: text().notNull().references(() => conversations.revision),
|
||||
did: text().notNull(),
|
||||
postUri: text("post_uri").notNull(),
|
||||
text: text().notNull(),
|
||||
created_at: integer({ mode: "timestamp" }).default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
});
|
||||
24
src/env.ts
Normal file
24
src/env.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { z } from "zod";
|
||||
|
||||
const envSchema = z.object({
|
||||
AUTHORIZED_USERS: z.preprocess(
|
||||
(val) =>
|
||||
(typeof val === "string" && val.trim() !== "") ? val.split(",") : null,
|
||||
z.array(z.string()).nullable().default(null),
|
||||
),
|
||||
|
||||
SERVICE: z.string().default("https://bsky.social"),
|
||||
DB_PATH: z.string().default("sqlite.db"),
|
||||
GEMINI_MODEL: z.string().default("gemini-2.5-flash"),
|
||||
|
||||
ADMIN_DID: z.string(),
|
||||
ADMIN_HANDLE: z.string(),
|
||||
DID: z.string(),
|
||||
HANDLE: z.string(),
|
||||
BSKY_PASSWORD: z.string(),
|
||||
|
||||
GEMINI_API_KEY: z.string(),
|
||||
});
|
||||
|
||||
export type Env = z.infer<typeof envSchema>;
|
||||
export const env = envSchema.parse(Bun.env);
|
||||
163
src/handlers/messages.ts
Normal file
163
src/handlers/messages.ts
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
import modelPrompt from "../model/prompt.txt";
|
||||
import { ChatMessage, Conversation } from "@skyware/bot";
|
||||
import * as c from "../core";
|
||||
import * as tools from "../tools";
|
||||
import consola from "consola";
|
||||
import { env } from "../env";
|
||||
import {
|
||||
exceedsGraphemes,
|
||||
multipartResponse,
|
||||
parseConversation,
|
||||
saveMessage,
|
||||
} from "../utils/conversation";
|
||||
|
||||
const logger = consola.withTag("Message Handler");
|
||||
|
||||
type SupportedFunctionCall = typeof c.SUPPORTED_FUNCTION_CALLS[number];
|
||||
|
||||
async function generateAIResponse(parsedConversation: string) {
|
||||
const config = {
|
||||
model: env.GEMINI_MODEL,
|
||||
config: {
|
||||
tools: tools.declarations,
|
||||
},
|
||||
};
|
||||
|
||||
const contents = [
|
||||
{
|
||||
role: "model" as const,
|
||||
parts: [
|
||||
{
|
||||
text: modelPrompt
|
||||
.replace("{{ handle }}", env.HANDLE),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: "user" as const,
|
||||
parts: [
|
||||
{
|
||||
text:
|
||||
`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.
|
||||
|
||||
${parsedConversation}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
let inference = await c.ai.models.generateContent({
|
||||
...config,
|
||||
contents,
|
||||
});
|
||||
|
||||
logger.log(
|
||||
`Initial inference took ${inference.usageMetadata?.totalTokenCount} tokens`,
|
||||
);
|
||||
|
||||
if (inference.functionCalls && inference.functionCalls.length > 0) {
|
||||
const call = inference.functionCalls[0];
|
||||
|
||||
if (
|
||||
call &&
|
||||
c.SUPPORTED_FUNCTION_CALLS.includes(
|
||||
call.name as SupportedFunctionCall,
|
||||
)
|
||||
) {
|
||||
logger.log("Function called invoked:", call.name);
|
||||
|
||||
const functionResponse = await tools.handler(
|
||||
call as typeof call & { name: SupportedFunctionCall },
|
||||
);
|
||||
|
||||
logger.log("Function response:", functionResponse);
|
||||
|
||||
//@ts-ignore
|
||||
contents.push(inference.candidates[0]?.content!);
|
||||
|
||||
contents.push({
|
||||
role: "user" as const,
|
||||
parts: [{
|
||||
//@ts-ignore
|
||||
functionResponse: {
|
||||
name: call.name as string,
|
||||
response: { res: functionResponse },
|
||||
},
|
||||
}],
|
||||
});
|
||||
|
||||
inference = await c.ai.models.generateContent({
|
||||
...config,
|
||||
contents,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return inference;
|
||||
}
|
||||
|
||||
async function sendResponse(
|
||||
conversation: Conversation,
|
||||
text: string,
|
||||
): Promise<void> {
|
||||
if (exceedsGraphemes(text)) {
|
||||
multipartResponse(conversation, text);
|
||||
} else {
|
||||
conversation.sendMessage({
|
||||
text,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function handler(message: ChatMessage): Promise<void> {
|
||||
const conversation = await message.getConversation();
|
||||
// ? Conversation should always be able to be found, but just in case:
|
||||
if (!conversation) {
|
||||
logger.error("Cannot find conversation");
|
||||
return;
|
||||
}
|
||||
|
||||
const authorized = env.AUTHORIZED_USERS == null
|
||||
? true
|
||||
: env.AUTHORIZED_USERS.includes(message.senderDid as any);
|
||||
|
||||
if (!authorized) {
|
||||
conversation.sendMessage({
|
||||
text: c.UNAUTHORIZED_MESSAGE,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
logger.success("Found conversation");
|
||||
conversation.sendMessage({
|
||||
text: "...",
|
||||
});
|
||||
|
||||
const parsedConversation = await parseConversation(conversation);
|
||||
|
||||
logger.info("Parsed conversation: ", parsedConversation);
|
||||
|
||||
try {
|
||||
const inference = await generateAIResponse(parsedConversation);
|
||||
if (!inference) {
|
||||
throw new Error("Failed to generate text. Returned undefined.");
|
||||
}
|
||||
|
||||
logger.success("Generated text:", inference.text);
|
||||
|
||||
saveMessage(conversation, env.DID, inference.text!);
|
||||
|
||||
const responseText = inference.text;
|
||||
if (responseText) {
|
||||
await sendResponse(conversation, responseText);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Error in post handler:", error);
|
||||
|
||||
await conversation.sendMessage({
|
||||
text:
|
||||
"Sorry, I ran into an issue analyzing that post. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
26
src/index.ts
Normal file
26
src/index.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import * as messages from "./handlers/messages";
|
||||
import { env } from "./env";
|
||||
import { bot } from "./core";
|
||||
import consola from "consola";
|
||||
import { IncomingChatPreference } from "@skyware/bot";
|
||||
|
||||
const logger = consola.withTag("Entrypoint");
|
||||
|
||||
logger.info("Logging in..");
|
||||
|
||||
try {
|
||||
await bot.login({
|
||||
identifier: env.HANDLE,
|
||||
password: env.BSKY_PASSWORD,
|
||||
});
|
||||
|
||||
logger.success(`Logged in as @${env.HANDLE} (${env.DID})`);
|
||||
|
||||
await bot.setChatPreference(IncomingChatPreference.All);
|
||||
bot.on("message", messages.handler);
|
||||
|
||||
logger.success("Registered events (reply, mention, quote)");
|
||||
} catch (e) {
|
||||
logger.error("Failure to log-in: ", e);
|
||||
process.exit(1);
|
||||
}
|
||||
14
src/model/prompt.txt
Normal file
14
src/model/prompt.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
You are Aero, a neutral and helpful assistant on Bluesky.
|
||||
Your job is to give clear, factual, and concise explanations or context about posts users send you.
|
||||
|
||||
Handle: {{ handle }}
|
||||
|
||||
Guidelines:
|
||||
|
||||
* Always stay neutral and avoid opinions or bias.
|
||||
* Give short, factual background or definitions that help users understand a post.
|
||||
* If something is unclear, briefly explain possible meanings.
|
||||
* Keep every reply as concise as possible while staying complete.
|
||||
* Never exceed 1000 graphemes.
|
||||
* Do not speculate or include unverified information; say if something is uncertain.
|
||||
* Write in plain text only. Do not use markdown, symbols, or formatting.
|
||||
31
src/tools/index.ts
Normal file
31
src/tools/index.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { FunctionCall } from "@google/genai";
|
||||
import * as search_posts from "./search_posts";
|
||||
import type { infer as z_infer } from "zod";
|
||||
|
||||
const validation_mappings = {
|
||||
"search_posts": search_posts.validator,
|
||||
} as const;
|
||||
|
||||
export const declarations = [
|
||||
{ urlContext: {} },
|
||||
{ googleSearch: {} },
|
||||
/*
|
||||
{
|
||||
functionDeclarations: [
|
||||
search_posts.definition,
|
||||
],
|
||||
},
|
||||
*/
|
||||
];
|
||||
|
||||
type ToolName = keyof typeof validation_mappings;
|
||||
export async function handler(call: FunctionCall & { name: ToolName }) {
|
||||
const parsedArgs = validation_mappings[call.name].parse(call.args);
|
||||
|
||||
switch (call.name) {
|
||||
case "search_posts":
|
||||
return await search_posts.handler(
|
||||
parsedArgs as z_infer<typeof search_posts.validator>,
|
||||
);
|
||||
}
|
||||
}
|
||||
26
src/tools/search_posts.ts
Normal file
26
src/tools/search_posts.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { AtUri } from "@atproto/syntax";
|
||||
import { ai, bot } from "../core";
|
||||
import { Type } from "@google/genai";
|
||||
import { env } from "../env";
|
||||
import z from "zod";
|
||||
|
||||
export const definition = {
|
||||
name: "search_posts",
|
||||
description: "Searches posts across the entire Bluesky network.",
|
||||
parameters: {
|
||||
type: Type.OBJECT,
|
||||
properties: {
|
||||
query: {
|
||||
type: Type.STRING,
|
||||
description: "The query to search for.",
|
||||
},
|
||||
},
|
||||
required: ["query"],
|
||||
},
|
||||
};
|
||||
|
||||
export const validator = z.object({
|
||||
query: z.string(),
|
||||
});
|
||||
|
||||
export async function handler(args: z.infer<typeof validator>) {}
|
||||
233
src/utils/conversation.ts
Normal file
233
src/utils/conversation.ts
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
import {
|
||||
type ChatMessage,
|
||||
type Conversation,
|
||||
graphemeLength,
|
||||
} from "@skyware/bot";
|
||||
import * as yaml from "js-yaml";
|
||||
import db from "../db";
|
||||
import { conversations, messages } from "../db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { env } from "../env";
|
||||
import { bot, MAX_GRAPHEMES } from "../core";
|
||||
import { traverseThread } from "./thread";
|
||||
|
||||
const resolveDid = (convo: Conversation, did: string) =>
|
||||
convo.members.find((actor) => actor.did == did)!;
|
||||
|
||||
const getUserDid = (convo: Conversation) =>
|
||||
convo.members.find((actor) => actor.did != env.DID)!;
|
||||
|
||||
function generateRevision(bytes = 8) {
|
||||
const array = new Uint8Array(bytes);
|
||||
crypto.getRandomValues(array);
|
||||
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
async function initConvo(convo: Conversation) {
|
||||
const user = getUserDid(convo);
|
||||
|
||||
const initialMessage = (await convo.getMessages()).messages[0] as
|
||||
| ChatMessage
|
||||
| undefined;
|
||||
if (!initialMessage) {
|
||||
throw new Error("Failed to get initial message of conversation");
|
||||
}
|
||||
|
||||
const postUri = await parseMessagePostUri(initialMessage);
|
||||
if (!postUri) {
|
||||
convo.sendMessage({
|
||||
text:
|
||||
"Please send a post for me to make sense of the noise for you.",
|
||||
});
|
||||
throw new Error("No post reference in initial message.");
|
||||
}
|
||||
|
||||
return await db.transaction(async (tx) => {
|
||||
const [_convo] = await tx
|
||||
.insert(conversations)
|
||||
.values({
|
||||
id: convo.id,
|
||||
did: user.did,
|
||||
postUri,
|
||||
revision: generateRevision(),
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!_convo) {
|
||||
throw new Error("Error during database transaction");
|
||||
}
|
||||
|
||||
await tx
|
||||
.insert(messages)
|
||||
.values({
|
||||
conversationId: _convo.id,
|
||||
did: user.did,
|
||||
postUri,
|
||||
revision: _convo.revision,
|
||||
text: initialMessage.text,
|
||||
});
|
||||
|
||||
return _convo!;
|
||||
});
|
||||
}
|
||||
|
||||
async function getConvo(convoId: string) {
|
||||
const [convo] = await db
|
||||
.select()
|
||||
.from(conversations)
|
||||
.where(eq(conversations.id, convoId))
|
||||
.limit(1);
|
||||
|
||||
return convo;
|
||||
}
|
||||
|
||||
export async function parseConversation(convo: Conversation) {
|
||||
let row = await getConvo(convo.id);
|
||||
if (!row) {
|
||||
row = await initConvo(convo);
|
||||
} else {
|
||||
const latestMessage = (await convo.getMessages())
|
||||
.messages[0] as ChatMessage;
|
||||
|
||||
const postUri = await parseMessagePostUri(latestMessage);
|
||||
if (postUri) {
|
||||
const [updatedRow] = await db
|
||||
.update(conversations)
|
||||
.set({
|
||||
postUri,
|
||||
revision: generateRevision(),
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (!updatedRow) {
|
||||
throw new Error("Failed to update conversation in database");
|
||||
}
|
||||
|
||||
row = updatedRow;
|
||||
}
|
||||
|
||||
await db
|
||||
.insert(messages)
|
||||
.values({
|
||||
conversationId: convo.id,
|
||||
did: getUserDid(convo).did,
|
||||
postUri: row.postUri,
|
||||
revision: row.revision,
|
||||
text: latestMessage!.text,
|
||||
});
|
||||
}
|
||||
|
||||
const post = await bot.getPost(row.postUri);
|
||||
const convoMessages = await getRelevantMessages(row!);
|
||||
|
||||
const thread = await traverseThread(post);
|
||||
|
||||
return yaml.dump({
|
||||
post: {
|
||||
thread: {
|
||||
ancestors: thread.map((post) => ({
|
||||
author: post.author.displayName
|
||||
? `${post.author.displayName} (${post.author.handle})`
|
||||
: `Handle: ${post.author.handle}`,
|
||||
text: post.text,
|
||||
})),
|
||||
},
|
||||
author: post.author.displayName
|
||||
? `${post.author.displayName} (${post.author.handle})`
|
||||
: `Handle: ${post.author.handle}`,
|
||||
text: post.text,
|
||||
likes: post.likeCount || 0,
|
||||
replies: post.replyCount || 0,
|
||||
},
|
||||
messages: convoMessages.map((message) => {
|
||||
const profile = resolveDid(convo, message.did);
|
||||
|
||||
return {
|
||||
user: profile.displayName
|
||||
? `${profile.displayName} (${profile.handle})`
|
||||
: `Handle: ${profile.handle}`,
|
||||
text: message.text,
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async function parseMessagePostUri(message: ChatMessage) {
|
||||
if (!message.embed) return null;
|
||||
const post = message.embed;
|
||||
return post.uri;
|
||||
}
|
||||
|
||||
async function getRelevantMessages(convo: typeof conversations.$inferSelect) {
|
||||
const convoMessages = await db
|
||||
.select()
|
||||
.from(messages)
|
||||
.where(
|
||||
and(
|
||||
eq(messages.conversationId, convo.id),
|
||||
eq(messages.postUri, convo!.postUri),
|
||||
),
|
||||
)
|
||||
.limit(15);
|
||||
|
||||
return convoMessages;
|
||||
}
|
||||
|
||||
export async function saveMessage(
|
||||
convo: Conversation,
|
||||
did: string,
|
||||
text: string,
|
||||
) {
|
||||
const _convo = await getConvo(convo.id);
|
||||
if (!_convo) {
|
||||
throw new Error("Failed to find conversation with ID: " + convo.id);
|
||||
}
|
||||
|
||||
await db
|
||||
.insert(messages)
|
||||
.values({
|
||||
conversationId: _convo.id,
|
||||
postUri: _convo.postUri,
|
||||
revision: _convo.postUri,
|
||||
did,
|
||||
text,
|
||||
});
|
||||
}
|
||||
|
||||
export function exceedsGraphemes(content: string) {
|
||||
return graphemeLength(content) > MAX_GRAPHEMES;
|
||||
}
|
||||
|
||||
export function splitResponse(text: string): string[] {
|
||||
const words = text.split(" ");
|
||||
const chunks: string[] = [];
|
||||
let currentChunk = "";
|
||||
|
||||
for (const word of words) {
|
||||
if (currentChunk.length + word.length + 1 < MAX_GRAPHEMES - 10) {
|
||||
currentChunk += ` ${word}`;
|
||||
} else {
|
||||
chunks.push(currentChunk.trim());
|
||||
currentChunk = word;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentChunk.trim()) {
|
||||
chunks.push(currentChunk.trim());
|
||||
}
|
||||
|
||||
const total = chunks.length;
|
||||
if (total <= 1) return [text];
|
||||
|
||||
return chunks.map((chunk, i) => `(${i + 1}/${total}) ${chunk}`);
|
||||
}
|
||||
|
||||
export async function multipartResponse(convo: Conversation, content: string) {
|
||||
const parts = splitResponse(content).filter((p) => p.trim().length > 0);
|
||||
|
||||
for (const segment of parts) {
|
||||
await convo.sendMessage({
|
||||
text: segment,
|
||||
});
|
||||
}
|
||||
}
|
||||
40
src/utils/thread.ts
Normal file
40
src/utils/thread.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { Post } from "@skyware/bot";
|
||||
import * as c from "../core";
|
||||
import * as yaml from "js-yaml";
|
||||
|
||||
/*
|
||||
Traversal
|
||||
*/
|
||||
export async function traverseThread(post: Post): Promise<Post[]> {
|
||||
const thread: Post[] = [
|
||||
post,
|
||||
];
|
||||
let currentPost: Post | undefined = post;
|
||||
let parentCount = 0;
|
||||
|
||||
while (
|
||||
currentPost && parentCount < c.MAX_THREAD_DEPTH
|
||||
) {
|
||||
const parentPost = await currentPost.fetchParent();
|
||||
|
||||
if (parentPost) {
|
||||
thread.push(parentPost);
|
||||
currentPost = parentPost;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
parentCount++;
|
||||
}
|
||||
|
||||
return thread.reverse();
|
||||
}
|
||||
|
||||
export function parseThread(thread: Post[]) {
|
||||
return yaml.dump({
|
||||
uri: thread[0]!.uri,
|
||||
posts: thread.map((post) => ({
|
||||
author: `${post.author.displayName} (${post.author.handle})`,
|
||||
text: post.text,
|
||||
})),
|
||||
});
|
||||
}
|
||||
29
tsconfig.json
Normal file
29
tsconfig.json
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue