From 491808abdc26acb30a5e43a3daf0d501621c9b6a Mon Sep 17 00:00:00 2001 From: Index Date: Wed, 22 Oct 2025 02:50:46 -0500 Subject: [PATCH] init --- .env.example | 20 +++ .gitignore | 37 +++++ Dockerfile | 11 ++ README.md | 16 ++ bun.lock | 270 ++++++++++++++++++++++++++++++++ drizzle.config.ts | 7 + drizzle/0000_black_micromax.sql | 22 +++ drizzle/meta/0000_snapshot.json | 174 ++++++++++++++++++++ drizzle/meta/_journal.json | 13 ++ package.json | 36 +++++ src/core.ts | 23 +++ src/db/index.ts | 7 + src/db/migrate.ts | 8 + src/db/schema.ts | 30 ++++ src/env.ts | 24 +++ src/handlers/messages.ts | 163 +++++++++++++++++++ src/index.ts | 26 +++ src/model/prompt.txt | 14 ++ src/tools/index.ts | 31 ++++ src/tools/search_posts.ts | 26 +++ src/utils/conversation.ts | 233 +++++++++++++++++++++++++++ src/utils/thread.ts | 40 +++++ tsconfig.json | 29 ++++ 23 files changed, 1260 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 bun.lock create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_black_micromax.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 package.json create mode 100644 src/core.ts create mode 100644 src/db/index.ts create mode 100644 src/db/migrate.ts create mode 100644 src/db/schema.ts create mode 100644 src/env.ts create mode 100644 src/handlers/messages.ts create mode 100644 src/index.ts create mode 100644 src/model/prompt.txt create mode 100644 src/tools/index.ts create mode 100644 src/tools/search_posts.ts create mode 100644 src/utils/conversation.ts create mode 100644 src/utils/thread.ts create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ac73cf1 --- /dev/null +++ b/.env.example @@ -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="" \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3cb348 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5b006a4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM oven/bun:latest + +WORKDIR /app + +COPY package.json bun.lock ./ + +RUN bun install --frozen-lockfile + +COPY . . + +CMD ["bun", "start"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..d7efeab --- /dev/null +++ b/README.md @@ -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. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..ae7e23e --- /dev/null +++ b/bun.lock @@ -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=="], + } +} diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..9156b13 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + dialect: "sqlite", + schema: "./src/db/schema.ts", + out: "./drizzle", +}); diff --git a/drizzle/0000_black_micromax.sql b/drizzle/0000_black_micromax.sql new file mode 100644 index 0000000..3841bff --- /dev/null +++ b/drizzle/0000_black_micromax.sql @@ -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 +); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..6939f17 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..d9dbfa5 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1761119054619, + "tag": "0000_black_micromax", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..5247fad --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/core.ts b/src/core.ts new file mode 100644 index 0000000..7cce219 --- /dev/null +++ b/src/core.ts @@ -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; diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..e044fb1 --- /dev/null +++ b/src/db/index.ts @@ -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 }); diff --git a/src/db/migrate.ts b/src/db/migrate.ts new file mode 100644 index 0000000..ef22137 --- /dev/null +++ b/src/db/migrate.ts @@ -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" }); diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 0000000..372e932 --- /dev/null +++ b/src/db/schema.ts @@ -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(), +}); diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 0000000..df55af5 --- /dev/null +++ b/src/env.ts @@ -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; +export const env = envSchema.parse(Bun.env); diff --git a/src/handlers/messages.ts b/src/handlers/messages.ts new file mode 100644 index 0000000..0bc6a25 --- /dev/null +++ b/src/handlers/messages.ts @@ -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 { + if (exceedsGraphemes(text)) { + multipartResponse(conversation, text); + } else { + conversation.sendMessage({ + text, + }); + } +} + +export async function handler(message: ChatMessage): Promise { + 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.", + }); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..898337b --- /dev/null +++ b/src/index.ts @@ -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); +} diff --git a/src/model/prompt.txt b/src/model/prompt.txt new file mode 100644 index 0000000..8c75bcf --- /dev/null +++ b/src/model/prompt.txt @@ -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. diff --git a/src/tools/index.ts b/src/tools/index.ts new file mode 100644 index 0000000..22e04b6 --- /dev/null +++ b/src/tools/index.ts @@ -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, + ); + } +} diff --git a/src/tools/search_posts.ts b/src/tools/search_posts.ts new file mode 100644 index 0000000..cc2a136 --- /dev/null +++ b/src/tools/search_posts.ts @@ -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) {} diff --git a/src/utils/conversation.ts b/src/utils/conversation.ts new file mode 100644 index 0000000..75d1bd8 --- /dev/null +++ b/src/utils/conversation.ts @@ -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, + }); + } +} diff --git a/src/utils/thread.ts b/src/utils/thread.ts new file mode 100644 index 0000000..58ca6a9 --- /dev/null +++ b/src/utils/thread.ts @@ -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 { + 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, + })), + }); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -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 + } +}