diff --git a/.example.env b/.example.env index df43162..bbad7ec 100644 --- a/.example.env +++ b/.example.env @@ -1,3 +1,6 @@ OPENAI_API_KEY= DISCORD_TOKEN= -DISCORD_CLIENT_ID= \ No newline at end of file +DISCORD_CLIENT_ID= +CHANNEL_ID= +RENDER_GIT_COMMIT= +DATA_DIR= \ No newline at end of file diff --git a/.github/instructions/typescript.instructions.md b/.github/instructions/typescript.instructions.md new file mode 100644 index 0000000..1bb1e15 --- /dev/null +++ b/.github/instructions/typescript.instructions.md @@ -0,0 +1,8 @@ +--- +applyTo: "**/*.{ts,tsx}" +--- + +Prefer `type` to `interface` for defining types. +Prefer `=>` for arrow functions. +Don't include return type annotations unless necessary. +Exports should appear at the start of the file. diff --git a/packages/shared/main.ts b/packages/shared/main.ts deleted file mode 100644 index f67b2c6..0000000 --- a/packages/shared/main.ts +++ /dev/null @@ -1 +0,0 @@ -console.log("Hello via Bun!"); \ No newline at end of file diff --git a/packages/shared/package.json b/packages/shared/package.json index 169498b..6786984 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -5,7 +5,8 @@ "./kv": "./src/kv.ts", "./utils": "./src/utils.ts", "./log": "./src/log.ts", - "./reminders": "./src/reminders.ts" + "./reminders": "./src/reminders.ts", + "./errors": "./src/errors.ts" }, "devDependencies": { "@types/bun": "latest" diff --git a/packages/shared/src/errors.ts b/packages/shared/src/errors.ts new file mode 100644 index 0000000..8e1a376 --- /dev/null +++ b/packages/shared/src/errors.ts @@ -0,0 +1,20 @@ +export const getErrorMessage = (error: unknown): string => { + if (error instanceof Error) { + return error.message + } + return String(error) +} + +export const getErrorStack = (error: unknown): string | undefined => { + if (error instanceof Error) { + return error.stack + } + return undefined +} + +export const getErrorName = (error: unknown): string => { + if (error instanceof Error) { + return error.name + } + return "UnknownError" +} diff --git a/packages/shared/src/kv.ts b/packages/shared/src/kv.ts index 2d7ff3b..7349886 100644 --- a/packages/shared/src/kv.ts +++ b/packages/shared/src/kv.ts @@ -1,22 +1,6 @@ import { mkdir } from "node:fs/promises" import { dirname, join } from "node:path" import type { Reminder } from "./reminders" -import { timeBomb } from "./utils" - -export interface Keys { - threads: Record // threadId: previousResponseId - reminders: Reminder[] - todos: Record // todoId: todoText - evaluations: EvalData[] // evaluation data for dashboard import -} - -// Individual versions for each key type -const keyVersions = { - threads: 1, - reminders: 1, - todos: 1, - evaluations: 1, -} as const const set = async (key: T, value: Keys[T]) => { try { @@ -34,8 +18,8 @@ const update = async ( updateFn: (prev: Keys[T]) => Keys[T] | Promise ) => { try { - const currentValue = await get(key) - const newValue = await updateFn(currentValue ?? defaultValue) + const currentValue = await get(key, defaultValue) + const newValue = await updateFn(currentValue) return await set(key, newValue) } catch (error) { console.error(`Error updating key "${key}":`, error) @@ -101,6 +85,25 @@ const getStorePath = (key: T): string => { export default { set, get, remove, update } +export type Keys = { + threads: Record // threadId: thread metadata + reminders: Reminder[] + todos: Record // todoId: todoText + evaluations: EvalData[] // evaluation data for dashboard import +} + +export type TrackedThread = { + threadId: string + threadName: string +} + +const keyVersions: Record = { + threads: 1, + reminders: 1, + todos: 1, + evaluations: 1, +} as const + export type Conversation = { message: string; role: "user" | "assistant" } export type EvalData = { diff --git a/packages/spike/README.md b/packages/spike/README.md index 203d163..66dc654 100644 --- a/packages/spike/README.md +++ b/packages/spike/README.md @@ -1,15 +1,12 @@ -# spike +# Spike -To install dependencies: +# Installation -```bash -bun install -``` - -To run: - -```bash -bun run index.ts -``` - -This project was created using `bun init` in bun v1.2.15. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. +1. Clone this repository, d'uh. +1. You need to have [bun](https://bun.sh) installed. +1. Install deps with `bun install`. +1. Setup a dummy Discord bot and get your DISCORD*TOKEN and CLIENT_ID at https://discord.com/developers/applications *(Corey doesn't remember how to do this all this, so if pair with him if you run into problems. Then document it in here). +1. `cp .env.example .env` and fill in the values for everything. +1. Find a Discord channel id to use for testing and set it in `.env` as `CHANNEL_ID`. +1. There is a very simple auth server you can use to invite the bot to your server. Run it with `bun authServer`. +1. Run the bot with `bun bot:discord` diff --git a/packages/spike/package.json b/packages/spike/package.json index ae1276e..7905761 100644 --- a/packages/spike/package.json +++ b/packages/spike/package.json @@ -5,7 +5,8 @@ "private": true, "scripts": { "bot:cli": "bun run --watch src/cli", - "bot:discord": "bun run --watch src/discord" + "bot:discord": "bun run --watch src/discord", + "authServer": "bun run --watch src/discord/auth.ts" }, "prettier": { "printWidth": 110, @@ -13,6 +14,8 @@ }, "dependencies": { "@openai/agents": "^0.0.10", + "@workshop/kv": "workspace:*", + "@workshop/shared": "workspace:*", "discord.js": "^14.19.3", "luxon": "^3.6.1", "zod": "3.25.67" diff --git a/packages/spike/src/discord/auth.ts b/packages/spike/src/discord/auth.ts index 4b70c8c..23b004c 100644 --- a/packages/spike/src/discord/auth.ts +++ b/packages/spike/src/discord/auth.ts @@ -1,5 +1,6 @@ import { serve } from "bun" +const premissions = "300648825856" export const startAuthServer = async (port = "3000") => { const server = serve({ port, @@ -10,7 +11,7 @@ export const startAuthServer = async (port = "3000") => { `

Authenticate spike

- Authorize + Authorize `, { diff --git a/packages/spike/src/discord/index.ts b/packages/spike/src/discord/index.ts index bb5fd1d..82fdc4b 100644 --- a/packages/spike/src/discord/index.ts +++ b/packages/spike/src/discord/index.ts @@ -2,6 +2,7 @@ import { Client, GatewayIntentBits, Partials } from "discord.js" import { listenForEvents } from "./events" import { runCronJobs } from "./cron" import { alertAboutCrashLog, logCrash } from "./crash" +import { keepThreadsAlive } from "./threadKeepAlive" const client = new Client({ intents: [ @@ -18,6 +19,7 @@ const client = new Client({ await client.login(process.env.DISCORD_TOKEN) listenForEvents(client) +keepThreadsAlive(client) process.on("unhandledRejection", async (error) => { console.error("๐Ÿ’ฅ Unhandled promise rejection:", error) diff --git a/packages/spike/src/discord/threadKeepAlive.ts b/packages/spike/src/discord/threadKeepAlive.ts new file mode 100644 index 0000000..2b9e9ee --- /dev/null +++ b/packages/spike/src/discord/threadKeepAlive.ts @@ -0,0 +1,82 @@ +import { Client, type PrivateThreadChannel, type PublicThreadChannel } from "discord.js" +import KV, { type TrackedThread } from "@workshop/shared/kv" +import { getErrorMessage } from "@workshop/shared/errors" +import { timeBomb } from "@workshop/shared/utils" + +timeBomb("7-15-2025", "Get rid of all these console messages Corey!") + +const timeout = 1000 * 60 * 30 // 30 minutes +export const keepThreadsAlive = async (client: Client) => { + try { + if (!client.isReady() || client.guilds.cache.size === 0) { + console.log("๐Ÿ“ญ Bot not ready or no guilds available") + return + } + + const trackedThreads = await KV.get("threads", {}) + const activeThreadIds = new Set() + + // Scan all active threads and update our tracking + for (const guild of client.guilds.cache.values()) { + const fetchedThreads = await guild.channels.fetchActiveThreads() + + for (const [threadId, thread] of fetchedThreads.threads) { + const threadData: TrackedThread = { threadId, threadName: thread.name } + trackedThreads[threadId] = threadData + activeThreadIds.add(threadId) + } + + if (fetchedThreads.threads.size === 0) { + console.log(`๐Ÿ“ญ No active threads in ${guild.name}`) + } else { + console.log(`๐Ÿงต Found ${fetchedThreads.threads.size} active threads in ${guild.name}`) + } + } + + // Find threads to revive (any thread we're tracking that's not currently active) + for (const [threadId, threadData] of Object.entries(trackedThreads)) { + if (!activeThreadIds.has(threadId)) { + await reviveThread(client, threadData) + } + } + + await KV.set("threads", trackedThreads) + } catch (error) { + console.error("Error scanning and reviving threads:", error) + } finally { + setTimeout(() => keepThreadsAlive(client), timeout) + } +} + +const reviveThread = async (client: Client, threadData: TrackedThread) => { + try { + const thread = await client.channels.fetch(threadData.threadId) + + if (!thread?.isThread()) { + console.error(`โŒ Thread "${threadData.threadName}" no longer exists, removing from tracking`) + await KV.update("threads", {}, (threads) => { + delete threads[threadData.threadId] + return threads + }) + + return + } + + if (thread.archived || aboutToBeArchived(thread)) { + await thread.setArchived(false, "Revived by Spike - keeping important discussion alive") + console.log(`โœ… Revived thread: "${threadData.threadName}"`) + } else { + console.log(`๐ŸŸข Thread "${threadData.threadName}" is already active, no action needed`) + } + } catch (error) { + console.error(`๐Ÿšซ Failed to revive "${threadData.threadName}".`, getErrorMessage(error)) + } +} + +const aboutToBeArchived = (thread: PublicThreadChannel | PrivateThreadChannel) => { + const archiveTimestamp = thread.archiveTimestamp + if (!archiveTimestamp) return true // I'm not sure when this happens + + const timeUntilArchive = archiveTimestamp - Date.now() + return timeUntilArchive <= timeout +} diff --git a/packages/werewolf-ui/src/examples/cards.tsx b/packages/werewolf-ui/src/examples/cards.tsx index 061b81a..89a9eb1 100644 --- a/packages/werewolf-ui/src/examples/cards.tsx +++ b/packages/werewolf-ui/src/examples/cards.tsx @@ -6,6 +6,7 @@ import { Input } from "../lib/input" import { Select } from "../lib/select" import { Grid } from "../lib/grid" import { Divider } from "../lib/divider" +import type { AvatarProps } from "../lib/avatar" export const Cards = () => { return ( @@ -203,12 +204,22 @@ TeamCard = Card(
+ Isaac Flath + Library Creator + +

Isaac Flath

+

Isaac Flath

Library Creator

+ + Alexandria, VA + Alexandria, VA + Alexandria, VA +

Alexandria, VA

@@ -269,7 +280,6 @@ section_content =(('bell','Everything',"Email digest, mentions & all activity.") ) } - const DateCard = () => { const date: string = new Date().toISOString().slice(0, 16) return ( diff --git a/packages/werewolf-ui/src/index.css b/packages/werewolf-ui/src/index.css index 1b7545b..2f1a811 100644 --- a/packages/werewolf-ui/src/index.css +++ b/packages/werewolf-ui/src/index.css @@ -19,9 +19,13 @@ .my-*: margin-top: *; margin-bottom: *; } +body { + @apply bg-background text-foreground; +} + @theme { --color-background: #ffffff; - --color-foreground: #09090b; + --color-foreground: red; --color-primary: #18181b; --color-primary-foreground: #fafafa; diff --git a/packages/whiteboard/src/ml.ts b/packages/whiteboard/src/ml.ts deleted file mode 100644 index 8a0dc8e..0000000 --- a/packages/whiteboard/src/ml.ts +++ /dev/null @@ -1,8 +0,0 @@ -// This file has been replaced with AI-based shape detection -// See ai.ts for the current implementation using Gemini/OpenAI -// which is more suitable for hand-drawn shape detection on whiteboards - -export const detectShapesFromBuffer = (arrayBuffer: ArrayBuffer) => { - console.warn("detectShapesFromBuffer is deprecated. Use AI-based detection from ai.ts instead.") - return [] -}