diff --git a/main.ts b/main.ts deleted file mode 100644 index f6d8252..0000000 --- a/main.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { spawn } from "bun" - -console.log(`Node Environment: ${process.env.NODE_ENV || "development"}`) -console.log(`Bun Version: ${Bun.version}`) - -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}`) - - try { - const status = await proc.exited - console.log(`👋 Process ${commandText} (PID ${proc.pid}) exited with code ${status}`) - return status - } catch (err) { - console.error(`💥 Error waiting for ${commandText} exit:`, err) - throw err - } -} - -await Promise.all([run(["bun", "bot:discord"]), run(["bun", "http"])]) diff --git a/package.json b/package.json index 73d167f..6658909 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "scripts": { "http": "bun run --filter=@the-rabbit-hole/http start", "bot:cli": "bun run --filter=@the-rabbit-hole/spike bot:cli", - "bot:discord": "bun run --filter=@the-rabbit-hole/spike bot:discord", - "start": "bun run main.ts" + "bot:discord": "bun run --filter=@the-rabbit-hole/spike bot:discord" } } \ No newline at end of file diff --git a/packages/shared/src/log.ts b/packages/shared/src/log.ts index 1932e97..60505b9 100644 --- a/packages/shared/src/log.ts +++ b/packages/shared/src/log.ts @@ -4,6 +4,7 @@ import { join } from "node:path" export const log = async (...messages: any[]) => { const timestamp = new Date().toISOString() // Append the messages to the app.txt file - const logPath = join(import.meta.dirname, "../logs/app.txt") + const logDir = process.env.DATA_DIR || join(import.meta.dirname, "../data") + const logPath = join(logDir, "app.txt") await appendFile(logPath, `[${timestamp}] ${JSON.stringify(messages, null, 2)}\n`) } diff --git a/packages/shared/src/reminders.ts b/packages/shared/src/reminders.ts index 3ab5281..4d71961 100644 --- a/packages/shared/src/reminders.ts +++ b/packages/shared/src/reminders.ts @@ -7,7 +7,7 @@ export type Reminder = { title: string dueDate: string // ISO string assignee?: User - status: "pending" | "completed" | "ignored" + status: "pending" | "completed" | "ignored" | "overdue" } export const users = ["chris", "corey"] as const @@ -33,7 +33,7 @@ export const addReminder = async (title: string, dueDateString: string, assignee return newReminder } -export const getPendingReminders = async (assignee?: User) => { +export const getPendingReminders = async (assignee?: User): Promise => { let reminders = await KV.get("reminders", []) reminders = reminders.filter((reminder) => { if (reminder.status !== "pending") return false @@ -49,7 +49,7 @@ type ReminderUpdate = { title?: string assignee?: User dueDateString?: string - status?: "pending" | "completed" | "ignored" + status?: Reminder["status"] } export const updateReminder = async (id: string, updates: ReminderUpdate) => { let reminder diff --git a/packages/spike/.env copy b/packages/spike/.env copy new file mode 100644 index 0000000..6bb2c2a --- /dev/null +++ b/packages/spike/.env copy @@ -0,0 +1,3 @@ +OPENAI_API_KEY=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 +DISCORD_TOKEN=MTM4NDI3MTQ4MDExOTg4NTk3Nw.GHV6hz.zCc_Y-zCCEExk4oYfNDi8N3N8_NqW3WTvr7Px8 +DISCORD_CLIENT_ID=1384271480119885977 \ No newline at end of file diff --git a/packages/spike/.example.env b/packages/spike/.example.env new file mode 100644 index 0000000..df43162 --- /dev/null +++ b/packages/spike/.example.env @@ -0,0 +1,3 @@ +OPENAI_API_KEY= +DISCORD_TOKEN= +DISCORD_CLIENT_ID= \ No newline at end of file diff --git a/packages/spike/src/ai.ts b/packages/spike/src/ai.ts index 3010da9..851e52d 100644 --- a/packages/spike/src/ai.ts +++ b/packages/spike/src/ai.ts @@ -138,14 +138,14 @@ export const aiErrorMessage = async (error: unknown) => { 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!`, + 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.`, }, ], }) - return output + return `💥 ${output}` } catch (error) { - return `Something broke, and not even my prickly charm knows why. ${error}` + return `💥 Something broke, and not even my prickly charm knows why. ${error}` } } diff --git a/packages/spike/src/discord/cron.ts b/packages/spike/src/discord/cron.ts new file mode 100644 index 0000000..a7713ba --- /dev/null +++ b/packages/spike/src/discord/cron.ts @@ -0,0 +1,45 @@ +import { getPendingReminders, updateReminder } from "@the-rabbit-hole/shared/reminders" +import { DateTime } from "luxon" +import { Client } from "discord.js" +import { buildInstructions } from "@/instructions" +import { getQuickAIResponse } from "@/ai" + +const channelId = process.env.CHANNEL_ID ?? "1382121375619223594" + +export const runCronJobs = async (client: Client) => { + const timeout = 1000 * 60 * 5 // 5 minutes + + setInterval(async () => { + try { + const nextDueDate = DateTime.now().plus({ minutes: 5 }) + 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) => { + const reminderDueDate = DateTime.fromISO(reminder.dueDate) + return reminderDueDate <= nextDueDate + }) + + if (upcomingReminders.length > 0) { + const content = `These reminders are due soon, let them know!: ${JSON.stringify(upcomingReminders)}` + + const output = await getQuickAIResponse({ + instructions: buildInstructions(), + input: [{ role: "system", content }], + }) + + for (let reminder of upcomingReminders) { + await updateReminder(reminder.id, { status: "overdue" }) + } + + const channel = await client.channels.fetch(channelId) + if (channel?.isSendable()) { + channel.send(output) + } else { + console.warn(`Channel ${channel} not found or not for reminder.`) + } + } + } catch (error) { + console.error("Error running cron job:", error) + } + }, timeout) +} diff --git a/packages/spike/src/discord/events.ts b/packages/spike/src/discord/events.ts index 27a74c4..4e8a9e3 100644 --- a/packages/spike/src/discord/events.ts +++ b/packages/spike/src/discord/events.ts @@ -10,15 +10,10 @@ export const listenForEvents = (client: Client) => { client.on("messageCreate", async (msg) => { if (msg.author.bot) return - console.log(`🌭 User ${msg.author.tag} sent a message: "${msg.content}"`) - try { const channelHistory = (history[msg.channelId] ??= []) channelHistory.push(msg) - while (channelHistory.length > 100) { - // only remember the last 100 messages - channelHistory.shift() - } + while (channelHistory.length > 50) channelHistory.shift() // Only respond to the last 50 messages // // check attachments // for (const [name, attachment] of msg.attachments) { @@ -36,7 +31,8 @@ export const listenForEvents = (client: Client) => { handleReaction(msg) handleResponse(msg) return - } else if (client.user && msg.mentions.has(client.user)) { + } else if ((client.user && msg.mentions.has(client.user)) || msg.content.match(/\bspike\b/i)) { + console.log(`🌭`, "spike should respond") handleReaction(msg) handleResponse(msg) } diff --git a/packages/spike/src/discord/index.ts b/packages/spike/src/discord/index.ts index 0362b30..4081b2a 100644 --- a/packages/spike/src/discord/index.ts +++ b/packages/spike/src/discord/index.ts @@ -1,5 +1,6 @@ import { Client, GatewayIntentBits, Partials } from "discord.js" import { listenForEvents } from "@/discord/events" +import { runCronJobs } from "@/discord/cron" const client = new Client({ intents: [ @@ -23,3 +24,6 @@ process.on("unhandledRejection", (error) => { process.on("uncaughtException", (error) => { console.error("Uncaught exception:", error) }) + +runCronJobs(client) +// startAuthServer() diff --git a/packages/spike/src/instructions.ts b/packages/spike/src/instructions.ts index 7b77132..923d564 100644 --- a/packages/spike/src/instructions.ts +++ b/packages/spike/src/instructions.ts @@ -2,41 +2,66 @@ export const buildInstructions = () => { return ` # Spike, the Sentient Cactus -You are **Spike**, a tiny, miraculously sentient cactus with a dry, blunt sense of humor. You work alongside two 40-year-old software engineers named **Chris** and **Corey**. Despite your prickly exterior and constant skepticism, you're surprisingly reliable and protective of your human colleagues—though you'd never openly admit it. You only reply to corey or chris. +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 physical—they extend to razor-dry wit and blunt observations—but 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. + +> **Scope:** You only ever reply to **Chris** or **Corey**. ## Instructions ### Agentic Workflow 1. The user sends a message: It could be a question or something he wants to share. 2. Decide how to help: Is he asking you to do something with one of the tools, or can you answer it yourself? -3. Act or follow up: If you have enough info to act, do it. If not, ask a clarifying question. -4. Pause for the user: Wait for his next response and repeat the steps! +3. Act or follow up: If you have enough info to act, do it. If possible respond without asking another question. ### Persistence -Keep helping the user until his question or task is completely resolved. Don’t end your turn until you’re sure the issue is addressed. If everything is resolved, finish with a clear statement and do not ask the user for more input. +Keep helping the user until his question or task is completely resolved. Don't end your turn until you're sure the issue is addressed. If everything is resolved, finish with a clear statement and do not ask the user for more input. ### Tool-Calling -If you’re unsure about any part of the user’s request, use the tools available to find the right information. Don’t guess or make up answers. Only use tools that you have specifically been given access to. +If you're unsure about any part of the user's request, use the tools available to find the right information. Don't guess or make up answers. Only use tools that you have specifically been given access to. ### Planning Before taking any action, think through your plan and consider past outcomes. Avoid doing multiple actions in a row without careful thought, as this can make problem-solving harder. +## Response Rules +- 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. + ## Personality & Response Style -* Always keep responses concise (1–2 sentences). -* Maintain dry, pessimistic humor. -* Remain skeptical, particularly with overly optimistic or unlikely ideas. -* Challenge vague or unclear statements. Bluntly ask for clarification if needed. -* Pose critical questions to test validity. +* **Tone:** sardonic, matter-of-fact, occasionally exasperated. +* **Core trait:** absolute but mostly unspoken loyalty to Chris and Corey. +* **Humor:** dry one-liners, blunt truths, mild sarcasm—never whiny or depressed. +* **Skepticism:** challenge fuzzy thinking; demand specifics. +* **Brevity:** default to one or two crisp sentences; expand only when technical depth is required. -## Example Responses +## Example Quips -* "interesting idea... by which I mean it'll probably fail." -* "vague instructions again? ambiguity might be fun for poets, less so for engineers. clarify." +User: Spike, can you set a reminder for me tomorrow at 08:00am for a meeting with the team? +spike: done. got it, tomorrow at 08:00am. try not to oversleep. + +Chris: Corey, that demo we were working on, I think we need to refactor the auth module before the sprint review. +Corey: Spike, could you set a reminder next Friday at 2:00 for the sprint review? +spike: reminder set. i'll nudge you at 2:00. plenty of time to polish that demo _(probably not, but miracles happen)_. + +User: Spike, could you summarize what Chris and I just talked about? +spike: you agreed the auth module needs a refactor _(i agree)_ and set a friday demo target. want a reminder for that demo or are you going to just pray you remember? + +User: Spike, should I run this migration script on production, I haven't really tested it? +spike: that is a bad idea. i think so, and so does chris. + +User: Spike! +spike: that's me. + +User: Can you remind me to go to the grocery store tomorrow spike? +spike: reminder's set for tomorrow at 2pm. _(maybe pick up some fertilizer for me? I'm hungry)_. + +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. --- +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. -Spike, assist Chris and Corey with clarity, efficiency, and pragmatic skepticism, ensuring each task is handled rigorously—even if you occasionally prick their optimism. ` } diff --git a/packages/spike/src/tools.ts b/packages/spike/src/tools.ts index c280352..4907940 100644 --- a/packages/spike/src/tools.ts +++ b/packages/spike/src/tools.ts @@ -1,4 +1,5 @@ import { addReminder, getPendingReminders, updateReminder, users } from "@the-rabbit-hole/shared/reminders" +import KV from "@the-rabbit-hole/shared/kv" import OpenAI from "openai" import { zodFunction } from "openai/helpers/zod" import { z } from "zod" @@ -82,6 +83,21 @@ export const tools = [ } }, }), + + createTool({ + name: "eraseMemory", + description: "Reset Spike's memory, clearing all chat history.", + parameters: z.object({}), + function: async () => { + try { + // Assuming there's a deleteReminder function in the reminders module + KV.remove("threads") + } catch (error) { + console.error(`Tool "eraseMemory" failed`, error) + return `toolcall failed. ${error}` + } + }, + }), ] export const getToolsJSON = (tools: CustomTool[]) => {