revive threads
This commit is contained in:
parent
8e3ef39116
commit
a495a5c50c
|
|
@ -1,3 +1,6 @@
|
|||
OPENAI_API_KEY=
|
||||
DISCORD_TOKEN=
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_ID=
|
||||
CHANNEL_ID=
|
||||
RENDER_GIT_COMMIT=
|
||||
DATA_DIR=
|
||||
8
.github/instructions/typescript.instructions.md
vendored
Normal file
8
.github/instructions/typescript.instructions.md
vendored
Normal file
|
|
@ -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.
|
||||
|
|
@ -1 +0,0 @@
|
|||
console.log("Hello via Bun!");
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
20
packages/shared/src/errors.ts
Normal file
20
packages/shared/src/errors.ts
Normal file
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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<string, string> // threadId: previousResponseId
|
||||
reminders: Reminder[]
|
||||
todos: Record<string, string> // 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 <T extends keyof Keys>(key: T, value: Keys[T]) => {
|
||||
try {
|
||||
|
|
@ -34,8 +18,8 @@ const update = async <T extends keyof Keys>(
|
|||
updateFn: (prev: Keys[T]) => Keys[T] | Promise<Keys[T]>
|
||||
) => {
|
||||
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 = <T extends keyof Keys>(key: T): string => {
|
|||
|
||||
export default { set, get, remove, update }
|
||||
|
||||
export type Keys = {
|
||||
threads: Record<string, TrackedThread> // threadId: thread metadata
|
||||
reminders: Reminder[]
|
||||
todos: Record<string, string> // todoId: todoText
|
||||
evaluations: EvalData[] // evaluation data for dashboard import
|
||||
}
|
||||
|
||||
export type TrackedThread = {
|
||||
threadId: string
|
||||
threadName: string
|
||||
}
|
||||
|
||||
const keyVersions: Record<keyof Keys, number> = {
|
||||
threads: 1,
|
||||
reminders: 1,
|
||||
todos: 1,
|
||||
evaluations: 1,
|
||||
} as const
|
||||
|
||||
export type Conversation = { message: string; role: "user" | "assistant" }
|
||||
|
||||
export type EvalData = {
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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") => {
|
|||
`<html>
|
||||
<body>
|
||||
<h1>Authenticate spike</h1>
|
||||
<a href="https://discord.com/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&scope=bot&permissions=0">Authorize</a>
|
||||
<a href="https://discord.com/oauth2/authorize?client_id=${process.env.DISCORD_CLIENT_ID}&scope=bot&permissions=${premissions}">Authorize</a>
|
||||
</body>
|
||||
</html>`,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
82
packages/spike/src/discord/threadKeepAlive.ts
Normal file
82
packages/spike/src/discord/threadKeepAlive.ts
Normal file
|
|
@ -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<boolean> | PrivateThreadChannel) => {
|
||||
const archiveTimestamp = thread.archiveTimestamp
|
||||
if (!archiveTimestamp) return true // I'm not sure when this happens
|
||||
|
||||
const timeUntilArchive = archiveTimestamp - Date.now()
|
||||
return timeUntilArchive <= timeout
|
||||
}
|
||||
|
|
@ -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(
|
|||
<HStack gap="2" v="center">
|
||||
<Placeholder.Avatar size="20" rounded seed="Isaac Flath" />
|
||||
<div>
|
||||
<Title>Isaac Flath</Title>
|
||||
<Subtitle>Library Creator</Subtitle>
|
||||
|
||||
<H1>Isaac Flath</H1>
|
||||
|
||||
<h2>Isaac Flath</h2>
|
||||
<h4>Library Creator</h4>
|
||||
</div>
|
||||
</HStack>
|
||||
<HStack h="between" gap="3" v="center">
|
||||
<Icon name="MapPin" size={6} />
|
||||
|
||||
<Text small>Alexandria, VA</Text>
|
||||
<Text size="small">Alexandria, VA</Text>
|
||||
<Text.Small>Alexandria, VA</Text.Small>
|
||||
|
||||
<p class="grow">Alexandria, VA</p>
|
||||
<IconLink name="Mail" size={6} />
|
||||
<IconLink name="Linkedin" size={6} />
|
||||
|
|
@ -269,7 +280,6 @@ section_content =(('bell','Everything',"Email digest, mentions & all activity.")
|
|||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
const DateCard = () => {
|
||||
const date: string = new Date().toISOString().slice(0, 16)
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user