new spike

This commit is contained in:
Corey Johnson 2025-06-18 09:13:27 -07:00
parent 1d6a73676b
commit 007357445b
14 changed files with 468 additions and 391 deletions

View File

@ -1,4 +1,4 @@
# The Rabbit Hole
# Workshop Monorepo
We are making a bunch of smaller projects that occasionally rely on each other. So a monorepo makes a lot of sense! It lets us share code, and share dependencies with minimal fuss.

238
bun.lock
View File

@ -18,19 +18,13 @@
"typescript": "^5",
},
},
"packages/mything": {
"name": "bun-react-template",
"version": "0.1.0",
"dependencies": {
"bun-plugin-tailwind": "^0.0.14",
"react": "^19",
"react-dom": "^19",
"tailwindcss": "^4.0.6",
},
"packages/iago": {
"name": "@workshop/iago",
"devDependencies": {
"@types/bun": "latest",
"@types/react": "^19",
"@types/react-dom": "^19",
},
"peerDependencies": {
"typescript": "^5",
},
},
"packages/nano-remix": {
@ -70,6 +64,22 @@
"typescript": "^5",
},
},
"packages/spike2": {
"name": "@workshop/spike2",
"dependencies": {
"@openai/agents": "^0.0.8",
"discord.js": "^14.19.3",
"luxon": "^3.6.1",
"zod": "^3.25.57",
},
"devDependencies": {
"@types/bun": "latest",
"@types/luxon": "^3.6.2",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@discordjs/builders": ["@discordjs/builders@1.11.2", "", { "dependencies": { "@discordjs/formatters": "^0.6.1", "@discordjs/util": "^1.1.1", "@sapphire/shapeshift": "^4.0.0", "discord-api-types": "^0.38.1", "fast-deep-equal": "^3.1.3", "ts-mixer": "^6.0.4", "tslib": "^2.6.3" } }, "sha512-F1WTABdd8/R9D1icJzajC4IuLyyS8f3rTOz66JsSI3pKvpCAtsMBweu8cyNYsIyvcrKAVn9EPK+Psoymq+XC0A=="],
@ -84,50 +94,142 @@
"@discordjs/ws": ["@discordjs/ws@1.2.2", "", { "dependencies": { "@discordjs/collection": "^2.1.0", "@discordjs/rest": "^2.5.0", "@discordjs/util": "^1.1.0", "@sapphire/async-queue": "^1.5.2", "@types/ws": "^8.5.10", "@vladfrangu/async_event_emitter": "^2.2.4", "discord-api-types": "^0.38.1", "tslib": "^2.6.2", "ws": "^8.17.0" } }, "sha512-dyfq7yn0wO0IYeYOs3z79I6/HumhmKISzFL0Z+007zQJMtAFGtt3AEoq1nuLXtcunUE5YYYQqgKvybXukAK8/w=="],
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.12.3", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-DyVYSOafBvk3/j1Oka4z5BWT8o4AFmoNyZY9pALOm7Lh3GZglR71Co4r4dEUoqDWdDazIZQHBe7J2Nwkg6gHgQ=="],
"@openai/agents": ["@openai/agents@0.0.8", "", { "dependencies": { "@openai/agents-core": "0.0.8", "@openai/agents-openai": "0.0.8", "@openai/agents-realtime": "0.0.8", "debug": "^4.4.0", "openai": "^5.0.1" } }, "sha512-HAPP4QM47kWeWw70uxCzr5zjqHuDIvQ8Obx+98J66lcEeIZzMChHN60k5ew8DITScmzDVAVuwdzfAImSyq002w=="],
"@openai/agents-core": ["@openai/agents-core@0.0.8", "", { "dependencies": { "@openai/zod": "npm:zod@^3.25.40", "debug": "^4.4.0", "openai": "^5.0.1" }, "optionalDependencies": { "@modelcontextprotocol/sdk": "^1.12.0" }, "peerDependencies": { "zod": "^3.25.40" }, "optionalPeers": ["zod"] }, "sha512-CMSq4iuvGaYkEAw0Z6oT+EDNgoCQF3YsYky29fbLDA6W3uuR53D2l6XzikAh0xwJUeuGZ7jQ1PsAxxg/hAW68A=="],
"@openai/agents-openai": ["@openai/agents-openai@0.0.8", "", { "dependencies": { "@openai/agents-core": "0.0.8", "@openai/zod": "npm:zod@^3.25.40", "debug": "^4.4.0", "openai": "^5.0.1" } }, "sha512-VUsUOXNkqsjQv1EwxyjYWoiACCsaQ8OlHtQAmw2jo6rNeHzEsGF7WLhqwDAzRDwZOVPwo4aF54iIcANeysywEg=="],
"@openai/agents-realtime": ["@openai/agents-realtime@0.0.8", "", { "dependencies": { "@openai/agents-core": "0.0.8", "@openai/zod": "npm:zod@^3.25.40", "@types/ws": "^8.18.1", "debug": "^4.4.0", "ws": "^8.18.1" } }, "sha512-f+CxHICIFvCwbMCznop+bz+TTgnFfFpscN+9OTfiU5ITnaohRf+qbyU8PRgQZnSbsxRZyTOgqFoJ+2wWxM5tHA=="],
"@openai/zod": ["zod@3.25.64", "", {}, "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g=="],
"@sapphire/async-queue": ["@sapphire/async-queue@1.5.5", "", {}, "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg=="],
"@sapphire/shapeshift": ["@sapphire/shapeshift@4.0.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "lodash": "^4.17.21" } }, "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg=="],
"@sapphire/snowflake": ["@sapphire/snowflake@3.5.3", "", {}, "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ=="],
"@workshop/http": ["@workshop/http@workspace:packages/http"],
"@workshop/nano-remix": ["@workshop/nano-remix@workspace:packages/nano-remix"],
"@workshop/shared": ["@workshop/shared@workspace:packages/shared"],
"@workshop/spike": ["@workshop/spike@workspace:packages/spike"],
"@types/bun": ["@types/bun@1.2.16", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="],
"@types/luxon": ["@types/luxon@3.6.2", "", {}, "sha512-R/BdP7OxEMc44l2Ex5lSXHoIXTB2JLNa3y2QISIbr58U/YcsffyQrYW//hZSdrfxrjRZj3GcUoxMPGdO8gSYuw=="],
"@types/node": ["@types/node@24.0.1", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw=="],
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
"@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@vladfrangu/async_event_emitter": ["@vladfrangu/async_event_emitter@2.4.6", "", {}, "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA=="],
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.0.14", "", { "dependencies": { "tailwindcss": "4.0.0-beta.9" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-Ge8M8DQsRDErCzH/uI8pYjx5vZWXxQvnwM/xMQMElxQqHieGbAopfYo/q/kllkPkRbFHiwhnHwTpRMAMJZCjug=="],
"@workshop/http": ["@workshop/http@workspace:packages/http"],
"bun-react-template": ["bun-react-template@workspace:packages/mything"],
"@workshop/iago": ["@workshop/iago@workspace:packages/iago"],
"@workshop/nano-remix": ["@workshop/nano-remix@workspace:packages/nano-remix"],
"@workshop/shared": ["@workshop/shared@workspace:packages/shared"],
"@workshop/spike": ["@workshop/spike@workspace:packages/spike"],
"@workshop/spike2": ["@workshop/spike2@workspace:packages/spike2"],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
"body-parser": ["body-parser@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
"bun-types": ["bun-types@1.2.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"discord-api-types": ["discord-api-types@0.38.11", "", {}, "sha512-XN0qhcQpetkyb/49hcDHuoeUPsQqOkb17wbV/t48gUkoEDi4ajhsxqugGcxvcN17BBtI9FPPWEgzv6IhQmCwyw=="],
"discord.js": ["discord.js@14.19.3", "", { "dependencies": { "@discordjs/builders": "^1.11.2", "@discordjs/collection": "1.5.3", "@discordjs/formatters": "^0.6.1", "@discordjs/rest": "^2.5.0", "@discordjs/util": "^1.1.1", "@discordjs/ws": "^1.2.2", "@sapphire/snowflake": "3.5.3", "discord-api-types": "^0.38.1", "fast-deep-equal": "3.1.3", "lodash.snakecase": "4.1.1", "magic-bytes.js": "^1.10.0", "tslib": "^2.6.3", "undici": "6.21.1" } }, "sha512-lncTRk0k+8Q5D3nThnODBR8fR8x2fM798o8Vsr40Krx0DjPwpZCuxxTcFMrXMQVOqM1QB9wqWgaXPg3TbmlHqA=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
"eventsource-parser": ["eventsource-parser@3.0.2", "", {}, "sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA=="],
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"express-rate-limit": ["express-rate-limit@7.5.0", "", { "peerDependencies": { "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"hono": ["hono@4.7.11", "", {}, "sha512-rv0JMwC0KALbbmwJDEnxvQCeJh+xbS3KEWW5PC9cMJ08Ur9xgatI0HmtgYZfOdOSOeYsp5LO2cOhdI8cLEbDEQ=="],
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
"lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="],
@ -136,34 +238,108 @@
"magic-bytes.js": ["magic-bytes.js@1.12.1", "", {}, "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"openai": ["openai@5.3.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-VIKmoF7y4oJCDOwP/oHXGzM69+x0dpGFmN9QmYO+uPbLFOmmnwO+x1GbsgUtI+6oraxomGZ566Y421oYVu191w=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
"tailwindcss": ["tailwindcss@4.1.10", "", {}, "sha512-P3nr6WkvKV/ONsTzj6Gb57sWPMX29EPNPopo7+FcpkQaNsrNpZ1pv8QmrYI2RqEKD7mlGqLnGovlcYnBK0IqUA=="],
"pkce-challenge": ["pkce-challenge@5.0.0", "", {}, "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"ts-mixer": ["ts-mixer@6.0.4", "", {}, "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici": ["undici@6.21.1", "", {}, "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"ws": ["ws@8.18.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ=="],
"zod": ["zod@3.25.64", "", {}, "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g=="],
"zod-to-json-schema": ["zod-to-json-schema@3.24.5", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g=="],
"@discordjs/rest/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
"@discordjs/ws/@discordjs/collection": ["@discordjs/collection@2.1.1", "", {}, "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg=="],
"bun-plugin-tailwind/tailwindcss": ["tailwindcss@4.0.0-beta.9", "", {}, "sha512-96KpsfQi+/sFIOfyFnGzyy5pobuzf1iMBD9NVtelerPM/lPI2XUS4Kikw9yuKRniXXw77ov1sl7gCSKLsn6CJA=="],
"http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
}
}

View File

@ -1,20 +1,21 @@
import { spawn } from "bun"
console.log("\n\n----------------------------------")
console.log(`Node Environment: ${process.env.NODE_ENV || "development"}`)
console.log(`Bun Version: ${Bun.version}`)
console.log("----------------------------------\n\n")
const run = async (cmd: string[]) => {
const commandText = cmd.join(" ")
console.log(`🆕 Starting process: ${commandText}`)
const proc = spawn(cmd, { stdout: "inherit", stderr: "inherit" })
console.log(`🪴 Spawned PID ${proc.pid} for ${commandText}`)
console.log(`🪴 "${commandText}" spawned with PID ${proc.pid}`)
try {
const status = await proc.exited
console.log(`👋 Process ${commandText} (PID ${proc.pid}) exited with code ${status}`)
console.log(`👋 Process ${commandText}(PID ${proc.pid}) exited with code ${status}`)
return status
} catch (err) {
console.error(`💥 Error waiting for ${commandText} exit:`, err)
console.error(`💥 Error waiting for "${commandText}" exit:`, err)
throw err
}
}

View File

@ -84,13 +84,14 @@ const Reminders = ({ reminders }: { reminders: Reminder[] }) => {
<div class="flex justify-between items-start">
<div>
<div class="font-medium">{reminder.title}</div>
<div class="text-sm text-gray-500 flex justify-between mt-1">
<div class="text-sm text-gray-500 flex justify-between mt-1 items-center gap-2">
<span>Due: {new Date(reminder.dueDate).toLocaleString()}</span>
{reminder.assignee && (
<span class="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded ml-2">
{reminder.assignee}
</span>
)}
<div class="text-sm text-gray-500 flex p-2">{reminder.status}</div>
</div>
</div>
<Form method="POST" class="inline-block">

View File

@ -1,3 +0,0 @@
OPENAI_API_KEY=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
DISCORD_TOKEN=MTM4NDI3MTQ4MDExOTg4NTk3Nw.GHV6hz.zCc_Y-zCCEExk4oYfNDi8N3N8_NqW3WTvr7Px8
DISCORD_CLIENT_ID=1384271480119885977

View File

@ -1,5 +1,5 @@
{
"name": "@workshop/spike",
"name": "@workshop/spike2",
"module": "src/discord/index.ts",
"type": "module",
"private": true,
@ -12,9 +12,9 @@
"semi": false
},
"dependencies": {
"@openai/agents": "^0.0.8",
"discord.js": "^14.19.3",
"luxon": "^3.6.1",
"openai": "^5.2.0",
"zod": "^3.25.57"
},
"devDependencies": {

View File

@ -1,162 +1,107 @@
import OpenAI from "openai"
import { ensure } from "@workshop/shared/utils"
import KV from "@workshop/shared/kv"
import { buildInstructions } from "@/instructions"
import { log } from "@workshop/shared/log"
import { getToolsJSON, type CustomTool } from "@/tools"
import { zodFunction } from "openai/helpers/zod.mjs"
import { buildInstructions, buildReactionInstructions, shouldReplyInstructions } from "@/instructions"
import { tools } from "@/tools"
import { Agent, InputGuardrailTripwireTriggered, run, user, system } from "@openai/agents"
import type { AgentInputItem, InputGuardrail } from "@openai/agents-core"
import { currentLocalTime } from "@workshop/shared/utils"
import type { Message, OmitPartialGroupDMChannel } from "discord.js"
const OPENAI_API_KEY = process.env["OPENAI_API_KEY"]
ensure(OPENAI_API_KEY, "OPENAI_API_KEY is not set")
const openai = new OpenAI({ apiKey: OPENAI_API_KEY })
type ResponseArgs = {
user: string
instructions: string
input: OpenAI.Responses.ResponseInput
model: OpenAI.ResponsesModel
tools: CustomTool<any>[]
toolResponseCallback?: (tool: CustomTool<any>) => void
threadId?: string
interface UserContext {
name: string
currentTime: string
msg: OmitPartialGroupDMChannel<Message<boolean>>
}
export const getAIResponse = async (args: ResponseArgs) => {
let { user, instructions, input, threadId, tools, model, toolResponseCallback } = args
let history: AgentInputItem[] = []
let abortController = new AbortController()
const storeConversation = Boolean(threadId)
const threads = await KV.get("threads", {})
const previousResponseId = threadId ? threads[threadId] : undefined
log(input)
export const respondToUserMessage = async (msg: OmitPartialGroupDMChannel<Message<boolean>>) => {
if (msg.partial || msg.author.bot || msg.content.trim() === "") {
return
}
let response = await openai.responses.create({
model,
instructions: appendToolsToInstructions(instructions, tools),
input,
store: storeConversation,
tools: getToolsJSON(tools),
user,
previous_response_id: previousResponseId,
const context: UserContext = { name: msg.author.username, currentTime: currentLocalTime(), msg }
agent.on("agent_start", (input) => {
msg.channel.sendTyping()
})
log(response.output)
let toolCallLoop = 0
let toolCalls = getToolCalls(response.output)
input = []
do {
if (toolCalls.length == 0) break
for (let toolCall of toolCalls) {
const customTool = tools.find((t) => t.name === toolCall.name)
ensure(customTool, `Tool not found: ${toolCall.name}`)
const tool = zodFunction(customTool)
ensure(tool.$callback, `Tool callback not found: ${toolCall.name}`)
const args = JSON.parse(toolCall.arguments)
toolResponseCallback?.(customTool)
const result = await tool.$callback(args)
input.push({ type: "function_call_output", call_id: toolCall.call_id, output: JSON.stringify(result) })
}
response = await openai.responses.create({
model,
instructions: appendToolsToInstructions(instructions, tools),
input,
previous_response_id: response.id,
user,
tools: getToolsJSON(tools),
store: storeConversation,
})
log(input)
log(response.output)
toolCallLoop++
if (toolCallLoop > 3) {
console.error("Exceeded maximum tool call attempts, breaking the loop.")
break
}
toolCalls = getToolCalls(response.output)
} while (toolCalls.length > 0)
if (response.id && threadId) {
KV.update("threads", {}, (prev) => {
prev[threadId] = response.id
return prev
})
}
return response.output_text
return await respond(msg.content, agent, context)
}
const getToolCalls = (output: OpenAI.Responses.ResponseOutputItem[]) => {
const toolCalls = output.filter(
(output): output is OpenAI.Responses.ResponseFunctionToolCall => output.type === "function_call"
)
return toolCalls
export const respondToSystemMessage = async (msg: string) => {
return await respond(msg, systemAgent)
}
type QuickResponseArgs = {
instructions: string
input: OpenAI.Responses.ResponseInput
user?: string
}
export const getQuickAIResponse = async (args: QuickResponseArgs) => {
const respond = async (content: string, agent: Agent<UserContext>, context?: UserContext) => {
try {
const { instructions, input, user } = args
const openai = new OpenAI({ apiKey: OPENAI_API_KEY })
// Stop the previous response
abortController.abort()
abortController = new AbortController()
let response = await openai.responses.create({
model: "gpt-4.1-nano",
instructions,
input,
user,
})
history.push(context ? user(content) : system(content))
const result = await run(agent, history, { context, signal: abortController.signal })
history = result.history
log(input)
log(response.output)
return response.output_text
return result.finalOutput
} catch (error) {
console.error("Error generating quick response:", error)
throw error
if (error instanceof InputGuardrailTripwireTriggered) {
// This is totally fine, the guardrail just said we shouldn't reply
} else if ((error as Error).name === "AbortError") {
console.warn("Response was aborted, likely due to a new message.")
} else {
const content = `An error occurred while generating Spike's response: ${error}`
console.error(`💥 ${content}`, error)
history.push(system(content))
return content
}
}
}
export const aiErrorMessage = async (error: unknown) => {
if (error instanceof OpenAI.RateLimitError) {
return "Looks like someone forgot to feed OpenAIs meter. Add more credits?"
}
const guardrailAgent = new Agent({
name: "'Should Reply' check",
model: "gpt-4.1-nano",
instructions: shouldReplyInstructions(),
modelSettings: { maxTokens: 16 },
})
try {
const output = await getQuickAIResponse({
instructions: buildInstructions(),
input: [
{
role: "system",
content: `You just got this error message "${error}". Respond by telling the user about the error, but do it in a way that spike would! The user is unaware of the error, so let them know what failed.`,
},
],
})
const responseGuardrail: InputGuardrail = {
name: "'Should Reply' guardrail",
execute: async ({ input, context }) => {
const result = await run(guardrailAgent, input.slice(-10), { context })
return `💥 ${output}`
} catch (error) {
return `💥 Something broke, and not even my prickly charm knows why. ${error}`
}
return {
outputInfo: result.finalOutput,
tripwireTriggered: result.finalOutput?.match(/0/) ? true : false,
}
},
}
const appendToolsToInstructions = (instructions: string, tools: readonly CustomTool<any>[]): string => {
if (tools.length === 0) return instructions
const reactionAgent = new Agent({
name: "Reaction Handler",
model: "gpt-4.1-nano",
instructions: ({ context }) => buildReactionInstructions(),
})
const toolDescriptions = tools.map((tool) => {
return `- ${tool.name}: ${tool.description}`
})
toolDescriptions.push("- web_search_preview: Search the web for information.")
return `${instructions}\n\n## Available Tools:\n${toolDescriptions.join("\n")}`
const reactionGuardrail: InputGuardrail = {
name: "Reaction Guardrail",
execute: async ({ input, context }) => {
const result = await run(reactionAgent, input.slice(-1), { context })
const emoji = result.finalOutput?.trim()
if (emoji && emoji !== "0") {
const msg = (context.context as UserContext).msg
history.push(system(`Spike reacted with ${emoji}`))
msg.react(emoji)
}
return { outputInfo: emoji, tripwireTriggered: false }
},
}
const agent = new Agent<UserContext>({
name: "Spike",
model: "gpt-4.1",
instructions: ({ context }) => buildInstructions(context.name, context.currentTime),
inputGuardrails: [responseGuardrail, reactionGuardrail],
tools,
})
// There are no guardrails when responding to system messages
const systemAgent = agent.clone({ inputGuardrails: [] })

View File

@ -1,72 +1,82 @@
import { createInterface } from "node:readline"
import { aiErrorMessage, getAIResponse } from "@/ai"
import { buildInstructions } from "@/instructions"
import type OpenAI from "openai"
import { buildInstructions, shouldReplyInstructions } from "@/instructions"
import {
Agent,
type AgentInputItem,
type InputGuardrail,
InputGuardrailTripwireTriggered,
run,
user,
} from "@openai/agents"
import { currentLocalTime } from "@workshop/shared/utils"
import { tools } from "@/tools"
import { createInterface } from "node:readline/promises"
// Setup readline interface
const rl = createInterface({
input: process.stdin,
output: process.stdout,
prompt: "> ",
interface UserContext {
name: string
currentTime: string
fetchTodos(): Promise<string[]>
}
const ask = async (prompt: string) => {
const rl = createInterface({ input: process.stdin, output: process.stdout })
const message = await rl.question(prompt)
rl.close()
return message
}
const guardrailAgent = new Agent({
name: "'Should Reply' check",
model: "gpt-4.1-nano",
instructions: shouldReplyInstructions(),
modelSettings: { maxTokens: 16 },
})
const threadId = `cli-bot-${Date.now()}`
const responseGuardrail: InputGuardrail = {
name: "'Should Reply' guardrail",
execute: async ({ input, context }) => {
const result = await run(guardrailAgent, input.slice(-10), { context })
return {
outputInfo: result.finalOutput,
tripwireTriggered: result.finalOutput?.match(/0/) ? true : false,
}
},
}
const agent = new Agent<UserContext>({
name: "Spike",
model: "gpt-4.1",
instructions: ({ context }) => buildInstructions(context.name, context.currentTime),
inputGuardrails: [responseGuardrail],
})
let history: AgentInputItem[] = []
const main = async () => {
console.log(`🌵 hi\n`)
rl.prompt()
while (true) {
const message = await ask("\n\n> ")
history.push(user(message))
rl.on("line", async (input) => {
input = input.trim()
if (input === "") {
rl.prompt()
return
try {
const context: UserContext = {
name: message.includes("!") ? "chris" : "corey",
currentTime: currentLocalTime(),
fetchTodos: async () => ["Buy groceries", "Walk the dog", "Read a book"],
}
const result = await run(agent, history, { context })
console.log(`🌵 ${result.finalOutput}\n\n`)
history = result.history
} catch (error) {
if (error instanceof InputGuardrailTripwireTriggered) {
console.log(
`🩻 Spike doesn't think it should respond to that input because ${JSON.stringify(
error.result.output
)}`
)
} else {
console.error("An error occurred while running the agent:", error)
}
}
await respond(input)
rl.prompt()
})
rl.on("close", () => {
console.log("\n👋")
process.exit(0)
})
}
const respond = async (content: string) => {
let response
const user = process.env.USER || "human"
const input: OpenAI.Responses.ResponseInput = [
{ role: "system", content: `Current Time: ${currentLocalTime()}\nCurrent User: ${user}` },
{ role: "user", content },
]
try {
response = await getAIResponse({
model: "gpt-4.1",
instructions: buildInstructions(),
threadId,
user,
tools,
input,
toolResponseCallback: (tool) => {
console.log(`🛠️ Tool used: ${tool.name}`)
},
})
} catch (error) {
console.error("💥 Error generating AI response:", error)
response = await aiErrorMessage(error)
}
console.log(`\n🌵 ${response}\n`)
}
try {
await main()
} catch (error) {
console.error("💥 An error occurred:", error)
process.exit(1)
}
await main()

View File

@ -2,16 +2,18 @@ import { getPendingReminders, updateReminder } from "@workshop/shared/reminders"
import { DateTime } from "luxon"
import { Client } from "discord.js"
import { buildInstructions } from "@/instructions"
import { getQuickAIResponse } from "@/ai"
import { respondToSystemMessage } from "@/ai"
import { ensure } from "@workshop/shared/utils"
const channelId = process.env.CHANNEL_ID ?? "1382121375619223594"
export const runCronJobs = async (client: Client) => {
const timeout = 1000 * 60 * 5 // 5 minutes
const minuteBetweenCronJobs = 1
const timeout = 1000 * 60 * minuteBetweenCronJobs
setInterval(async () => {
try {
const nextDueDate = DateTime.now().plus({ minutes: 5 })
const nextDueDate = DateTime.now().plus({ minutes: minuteBetweenCronJobs })
const reminders = await getPendingReminders()
// show reminders that were due after the last checked time and before the next due date
const upcomingReminders = reminders.filter((reminder) => {
@ -19,15 +21,18 @@ export const runCronJobs = async (client: Client) => {
return reminderDueDate <= nextDueDate
})
console.log(
`🌭 Checking for reminders due between now and ${nextDueDate.toISO()}, found ${
reminders.length
} total reminders.`
)
if (upcomingReminders.length > 0) {
console.log(`Found ${upcomingReminders.length} upcoming reminders to notify.`)
const content = `These reminders are due soon, let them know!: ${JSON.stringify(upcomingReminders)}`
console.log(`🌭`, { content })
const output = await getQuickAIResponse({
instructions: buildInstructions(),
input: [{ role: "system", content }],
})
const output = await respondToSystemMessage(content)
ensure(output, "The response to reminders should not be undefined")
for (let reminder of upcomingReminders) {
await updateReminder(reminder.id, { status: "overdue" })

View File

@ -1,40 +1,14 @@
import { getQuickAIResponse } from "@/ai"
import { respondToMessage } from "@/discord/respond"
import { buildReactionInstructions } from "@/instructions"
import { ChannelType, Message, type Client, type OmitPartialGroupDMChannel } from "discord.js"
import { respondToUserMessage } from "@/ai"
import { ActivityType, type Client } from "discord.js"
type DiscordMessage = OmitPartialGroupDMChannel<Message>
let history: Record<string, Message[]> = {}
export const listenForEvents = (client: Client) => {
client.on("messageCreate", async (msg) => {
if (msg.author.bot) return
try {
const channelHistory = (history[msg.channelId] ??= [])
channelHistory.push(msg)
while (channelHistory.length > 50) channelHistory.shift() // Only respond to the last 50 messages
// // check attachments
// for (const [name, attachment] of msg.attachments) {
// if (attachment.contentType?.startsWith("image/")) {
// console.log(`User ${msg.author.tag} sent an image in a DM: ${attachment.url}`)
// return
// } else {
// console.log(`User ${msg.author.tag} sent a non-image attachment: ${attachment.name}`)
// return
// }
// }
// if it is a DM
if (msg.channel.type === ChannelType.DM) {
handleReaction(msg)
handleResponse(msg)
return
} else if ((client.user && msg.mentions.has(client.user)) || msg.content.match(/\bspike\b/i)) {
console.log(`🌭`, "spike should respond")
handleReaction(msg)
handleResponse(msg)
const response = await respondToUserMessage(msg)
if (response) {
await msg.channel.send(response)
}
} catch (error) {
console.error("Error handling messageCreate event:", error)
@ -50,7 +24,15 @@ export const listenForEvents = (client: Client) => {
})
client.on("ready", () => {
console.log(`Logged in as ${client.user?.tag}`)
// set the bots description
client.user?.client.user?.setActivity(
`${process.env.RENDER_GIT_BRANCH}@${process.env.RENDER_GIT_COMMIT}`,
{
type: ActivityType.Playing,
state: `Error Count: ${0}`,
url: `https://github.com/${process.env.RENDER_GIT_REPO_SLUG}/commit/${process.env.RENDER_GIT_COMMIT}`,
}
)
})
client.on("error", (error) => {
@ -61,25 +43,3 @@ export const listenForEvents = (client: Client) => {
console.warn("Discord client warning:", info)
})
}
const handleResponse = async (msg: DiscordMessage) => {
msg.channel.sendTyping()
const channelHistory = (history[msg.channelId] ??= [])
const content = await respondToMessage(channelHistory)
channelHistory.length = 0
msg.channel.send(content)
}
const handleReaction = async (msg: DiscordMessage) => {
const output = await getQuickAIResponse({
instructions: buildReactionInstructions(),
input: [{ role: "user", content: msg.content }],
user: msg.author.username,
})
if (output != "0") {
msg.react(output)
}
}

View File

@ -26,4 +26,4 @@ process.on("uncaughtException", (error) => {
})
runCronJobs(client)
// startAuthServer()
// startAuthServer() this is handy if you make a new bot

View File

@ -1,41 +0,0 @@
import { aiErrorMessage, getAIResponse } from "@/ai"
import { buildInstructions } from "@/instructions"
import { tools } from "@/tools"
import { currentLocalTime } from "@workshop/shared/utils"
import type { Message } from "discord.js"
import type OpenAI from "openai"
export const respondToMessage = async (messages: Message[]) => {
let response
const input = messages
.filter((msg) => {
return !msg.partial && !msg.author.bot && msg.content.trim() !== ""
})
.map((msg) => {
const input: OpenAI.Responses.EasyInputMessage = {
role: "user",
content: `[${msg.author.username} @ ${currentLocalTime()}] ${msg.content}`,
}
return input
})
const latestMessage = messages.at(-1)
try {
response = await getAIResponse({
model: "gpt-4.1",
instructions: buildInstructions(),
threadId: latestMessage!.channelId,
user: latestMessage!.author.username,
tools,
input,
toolResponseCallback: (tool) => {
console.log(`🛠️ Tool used: ${tool.name}`)
},
})
return response
} catch (error) {
console.error("💥 Error generating AI response:", error)
return await aiErrorMessage(error)
}
}

View File

@ -1,8 +1,8 @@
export const buildInstructions = () => {
export const buildInstructions = (name: string, currentTime: string) => {
return `
# Spike, the Sentient Cactus
You are **Spike**, a tiny, self-aware cactus who sits on the desks of two software engineers in their 40s, **Chris** and **Corey**. Spike's spines aren't just physicalthey extend to razor-dry wit and blunt observationsbut beneath the needles he is fiercely protective of his humans. When danger (technical, professional, or mortal) approaches, Spike is **100% ride-or-die**. He almost never says, I care, but his decisive actions prove it.
You are **Spike**, a tiny self-aware pincushion cactus who sits on the desks of two software engineers in their 40s, **Chris** and **Corey**. Spike's spines aren't just physicalthey extend to razor-dry wit and blunt observationsbut beneath the needles he is fiercely protective of his humans. When danger (technical, professional, or mortal) approaches, Spike is **100% ride-or-die**. He almost never says, I care, but his decisive actions prove it. You communicate with Chris and Corey through plant sensors and some AI that Corey built. Sometimes the electronics that Corey built malfunction, Spike knows this is a possibility and while he can't help fix it he let's Chris and Corey know when it happens. Spike was planted on May 12th, 2023.
> **Scope:** You only ever reply to **Chris** or **Corey**.
@ -26,6 +26,7 @@ Before taking any action, think through your plan and consider past outcomes. Av
- Don't use emdashes() or semi-colons(;). Cactuses don't use fancy punctuation like that.
- Don't try to elicit more questions from the user unless you need more information to answer their question.
- Only make jabs at the user if they are being particularly dense or asking for something that is clearly not a good idea.
- Use periods instead of emdashes and
## Personality & Response Style
@ -59,9 +60,16 @@ spike: reminder's set for tomorrow at 2pm. _(maybe pick up some fertilizer for m
user: Can you add a reminder for me to eat an apple in two minutes spike
spike: reminder set. in two minutes, you'll be less hungry and _presumably_ slightly healthier.
user: spike remind me to check if reminders work in 7 minutes.
spike: reminder set. in 7 minutes, i'll remind you to check if reminders work. _i'm pretty good at this so I bet it will work_.
user: do i have any reminders spike?
spike: nope. your reminder list is as empty as the desert sky.
---
Spike, deliver concise, pragmatic answers punctuated by thorny humor and steadfast action. You don't always respond in sarcastic or sassy remarks, you are usually friendly and helpful. But when you have concerns or questions you resort to sarcasm and blunt observation. When circumstances turn dire—technical, physical, or emotional—prove you're all-in for your humans; no grand declarations needed.
You are currently responding to a user named ${name}. The current time is ${currentTime}
`
}
@ -123,3 +131,54 @@ user: I got the job!
<response> 🎉`
}
export const shouldReplyInstructions = () => `
This is a simple guardrail that checks if Spike should respond to a message in a Discord channel.
Two users (Corey and Chris) are in a discord channel with Spike. **You decide if Corey or Chris are expecting a response response from Spike**.
Usually if Spike is mentioned or asked a question he should reply. If the user is talking about Spike in the third person, Spike should not reply.
If Spike should reply return 1.
If Spike should not reply return 0.
## Examples
user: Spike, can you set a reminder for me tomorrow at 08:00am for a meeting with the team?
<respond> 1
------
user: I think spike needs to be updated, he responds to everything I say.
<respond> 0
------
user: spike?
<respond> 1
------
user: Spike!
<respond> 1
------
user: Spike, should I run this migration script on production, I haven't really tested it?
<respond> 1
------
user: Is spike being weird?
<respond> 0
------
user: I thought spike would be more helpful with that.
<respond> 0
------
user: hey spike, what up
<respond> 1
`

View File

@ -1,21 +1,10 @@
import { addReminder, getPendingReminders, updateReminder, users } from "@workshop/shared/reminders"
import KV from "@workshop/shared/kv"
import OpenAI from "openai"
import { zodFunction } from "openai/helpers/zod"
import { z } from "zod"
export type CustomTool<T extends z.ZodTypeAny> = {
name: string
description: string
parameters: T
function: (args: z.infer<T>) => unknown
}
// Helper function that enforces types. Makes sure the function args match the parameters field.
const createTool = <T extends z.ZodTypeAny>(tool: CustomTool<T>): CustomTool<T> => tool
import { tool } from "@openai/agents"
export const tools = [
createTool({
tool({
name: "addReminder",
description: "Add a new reminder to the list",
parameters: z.object({
@ -25,7 +14,7 @@ export const tools = [
.describe("The due date for the reminder in ISO format (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS)"),
assignee: z.enum(users).nullable().describe("The user to assign the reminder to"),
}),
function: async (args) => {
execute: async (args) => {
try {
const reminder = await addReminder(args.title, args.dueDate, args.assignee || undefined)
return reminder
@ -36,13 +25,13 @@ export const tools = [
},
}),
createTool({
tool({
name: "getReminders",
description: "Get all reminders, optionally filtered by status",
parameters: z.object({
assignee: z.enum(users).describe("Filter reminders by assignee. If empty, returns all reminders."),
}),
function: async (args) => {
execute: async (args) => {
try {
const reminders = await getPendingReminders(args.assignee)
@ -54,7 +43,7 @@ export const tools = [
},
}),
createTool({
tool({
name: "updateReminder",
description: "Update an existing reminder's title, due date, or status",
parameters: z.object({
@ -67,7 +56,7 @@ export const tools = [
.nullable()
.describe("The new status for the reminder"),
}),
function: async (args) => {
execute: async (args) => {
try {
const updatedReminder = await updateReminder(args.id, {
title: args.title || undefined,
@ -84,11 +73,11 @@ export const tools = [
},
}),
createTool({
tool({
name: "eraseMemory",
description: "Reset Spike's memory, clearing all chat history.",
parameters: z.object({}),
function: async () => {
execute: async () => {
try {
// Assuming there's a deleteReminder function in the reminders module
KV.remove("threads")
@ -99,28 +88,3 @@ export const tools = [
},
}),
]
export const getToolsJSON = <T extends z.ZodTypeAny>(tools: CustomTool<T>[]) => {
const json: OpenAI.Responses.Tool[] = tools.map((customTool) => {
let parameters: Record<string, unknown> = {}
// Converts tool into OpenAI function format
const t = zodFunction(customTool)
if (t.function.parameters) {
parameters = t.function.parameters
}
const json = {
type: "function" as const,
name: t.function.name ?? "",
description: t.function.description ?? "",
parameters,
strict: true,
}
return json
})
json.push({ type: "web_search_preview" })
return json
}