This commit is contained in:
Corey Johnson 2025-06-16 16:51:17 -07:00
parent 6ca88c8a76
commit 9a898c0554
12 changed files with 122 additions and 52 deletions

22
main.ts
View File

@ -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"])])

View File

@ -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"
}
}

View File

@ -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`)
}

View File

@ -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<Reminder[]> => {
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

3
packages/spike/.env copy Normal file
View File

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

View File

@ -0,0 +1,3 @@
OPENAI_API_KEY=
DISCORD_TOKEN=
DISCORD_CLIENT_ID=

View File

@ -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}`
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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 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.
> **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. Dont end your turn until youre 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 youre unsure about any part of the users request, use the tools available to find the right information. Dont 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 (12 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 sarcasmnever 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 rigorouslyeven if you occasionally prick their optimism.
`
}

View File

@ -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 = <T extends z.ZodTypeAny>(tools: CustomTool<T>[]) => {