This commit is contained in:
Corey Johnson 2025-07-22 10:35:26 -07:00
parent ca7378f62e
commit e68bf409f3
7 changed files with 34 additions and 310 deletions

View File

@ -5,7 +5,6 @@
"./kv": "./src/kv.ts",
"./utils": "./src/utils.ts",
"./log": "./src/log.ts",
"./reminders": "./src/reminders.ts",
"./errors": "./src/errors.ts"
},
"devDependencies": {

View File

@ -1,6 +1,5 @@
import { mkdir } from "node:fs/promises"
import { dirname, join } from "node:path"
import type { Reminder } from "./reminders"
const set = async <T extends keyof Keys>(key: T, value: Keys[T]) => {
try {
@ -87,7 +86,6 @@ 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
}
@ -99,7 +97,6 @@ export type TrackedThread = {
const keyVersions: Record<keyof Keys, number> = {
threads: 1,
reminders: 1,
todos: 1,
evaluations: 1,
} as const

View File

@ -1,87 +0,0 @@
import { DateTime } from "luxon"
import KV from "./kv"
import { ensure, zone } from "./utils.ts"
export type Reminder = {
id: string
title: string
dueDate: string // ISO string
assignee?: User
status: "pending" | "completed" | "ignored" | "overdue"
}
export const users = ["chris", "corey"] as const
export type User = (typeof users)[number]
export const addReminder = async (title: string, dueDateString: string, assignee?: User) => {
const dueDate = DateTime.fromISO(dueDateString, { zone })
ensure(dueDate.isValid, `Invalid due date "${dueDateString}"`)
const guid = crypto.randomUUID()
const newReminder: Reminder = {
id: guid,
title,
dueDate: dueDate.toISO(),
status: "pending",
assignee: assignee || undefined,
}
await KV.update("reminders", [], (reminders: Reminder[]) => {
return [...reminders, newReminder]
})
return newReminder
}
export const getPendingReminders = async (assignee?: User): Promise<Reminder[]> => {
let reminders = await KV.get("reminders", [])
reminders = reminders.filter((reminder) => {
if (reminder.status !== "pending") return false
if (assignee && reminder.assignee !== assignee) return false
return true
})
reminders.sort((a, b) => DateTime.fromISO(a.dueDate).toMillis() - DateTime.fromISO(b.dueDate).toMillis())
return reminders
}
type ReminderUpdate = {
title?: string
assignee?: User
dueDateString?: string
status?: Reminder["status"]
}
export const updateReminder = async (id: string, updates: ReminderUpdate) => {
let reminder
await KV.update("reminders", [], async (reminders: Reminder[]) => {
reminder = reminders.find((r) => r.id === id)
ensure(reminder, `Reminder with id "${id}" not found`)
reminder.title = updates.title ?? reminder.title
reminder.status = updates.status ?? reminder.status
reminder.assignee = updates.assignee ?? reminder.assignee
if (updates.dueDateString) {
const dueDate = DateTime.fromISO(updates.dueDateString, { zone })
ensure(dueDate.isValid, `Invalid due date "${updates.dueDateString}"`)
reminder.dueDate = dueDate.toISO()
}
return reminders
})
return reminder
}
export const deleteReminder = async (id: string) => {
let deletedReminder: Reminder | undefined
await KV.update("reminders", [], (reminders: Reminder[]) => {
const reminderIndex = reminders.findIndex((r) => r.id === id)
ensure(reminderIndex !== -1, `Reminder with id "${id}" not found`)
reminders.splice(reminderIndex, 1)
return reminders
})
return deletedReminder
}

View File

@ -1,9 +1,4 @@
import { getPendingReminders, updateReminder } from "@workshop/shared/reminders"
import { DateTime } from "luxon"
import { Client } from "discord.js"
import { buildInstructions } from "../instructions"
import { respondToSystemMessage } from "../ai"
import { ensure } from "@workshop/shared/utils"
const channelId = process.env.CHANNEL_ID ?? "1382121375619223594"
@ -13,32 +8,6 @@ export const runCronJobs = async (client: Client) => {
setInterval(async () => {
try {
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) => {
const reminderDueDate = DateTime.fromISO(reminder.dueDate)
return reminderDueDate <= nextDueDate
})
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)}`
const output = await respondToSystemMessage(content, channelId)
ensure(output, "The response to reminders should not be undefined")
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)
}

View File

@ -0,0 +1,33 @@
import { ensure } from "@workshop/shared/utils"
export const getLogs = async (type: "app" | "request" | "build" = "app") => {
const ownerId = "tea-d1vamb95pdvs73d1sgtg"
const resourceId = "srv-d1vrdqmuk2gs73eop8o0"
const url = new URL("https://api.render.com/v1/logs")
url.searchParams.set("type", type)
url.searchParams.set("ownerId", ownerId)
url.searchParams.set("direction", "backward")
url.searchParams.set("resource", resourceId)
url.searchParams.set("limit", "100")
const response = await fetch(url.toString(), {
method: "GET",
headers: {
accept: "application/json",
authorization: `Bearer ${process.env.RENDER_API_KEY}`,
},
})
if (!response.ok) {
throw new Error(`Failed to fetch logs: ${response.statusText}`)
}
const data = (await response.json()) as any
ensure(data.logs, "Expected logs to be an array")
const logs = data.logs.map((log: any) => {
const { id, labels, ...rest } = log
return rest
})
return logs
}

View File

@ -1,188 +1 @@
import { addReminder, getPendingReminders, updateReminder, users } from "@workshop/shared/reminders"
import kv from "@workshop/shared/kv"
import { z } from "zod"
import { tool } from "@openai/agents"
import { type TodoJSON, type TodoList, todoListToString } from "@workshop/todo"
import { type UserContext } from "./ai"
import { User } from "discord.js"
import { ensure, zone } from "@workshop/shared/utils"
import { DateTime } from "luxon"
// Recursive Zod schema for TodoJSON
const todoJSONSchema: z.ZodType<TodoJSON> = z.lazy(() =>
z.object({
done: z.boolean(),
indent: z.string().describe("Indentation string of the todo item"),
text: z.string(),
children: z.array(todoJSONSchema),
})
)
// Zod schema for TodoList
const todoListSchema: z.ZodType<TodoList> = z.array(
z.object({
title: z.string().nullable().optional(),
todos: z
.array(z.union([todoJSONSchema, z.string()]))
.describe("Array of TodoJSON objects or empty lines"),
})
)
export const tools = [
tool<any, UserContext>({
name: "getDiscordThreadInformation",
description:
"Get information about all threads in the current discord guild (including a link, name, and when it will be archived)",
parameters: z.object({}),
execute: async (args, runContext) => {
try {
const guild = runContext?.context.msg.guild
ensure(guild, "Guild is required for getThreadInformation tool")
const threads = await guild.channels.fetchActiveThreads()
const threadInfo = Array.from(threads.threads.values()).map((thread) => ({
name: thread.name,
archived: thread.archived,
link: thread.url,
archiveTimestamp: DateTime.fromMillis(thread.archiveTimestamp || 0)
.setZone(zone)
.toISO(),
}))
return threadInfo
} catch (error) {
console.error(`Tool "getThreadInformation" failed for "${args.channelId}":`, error)
return `toolcall failed. ${error}`
}
},
}),
tool({
name: "addReminder",
description: "Add a new reminder to the list",
parameters: z.object({
title: z.string().describe("The title or description of the reminder"),
dueDate: z
.string()
.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"),
}),
execute: async (args) => {
try {
const reminder = await addReminder(args.title, args.dueDate, args.assignee || undefined)
return reminder
} catch (error) {
console.error(`Tool "addReminder" failed for "${JSON.stringify(args)}":`, error)
return `toolcall failed. ${error}`
}
},
}),
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."),
}),
execute: async (args) => {
try {
const reminders = await getPendingReminders(args.assignee)
return reminders
} catch (error) {
console.error(`Tool "getReminders" failed`, error)
return `toolcall failed. ${error}`
}
},
}),
tool({
name: "updateReminder",
description: "Update an existing reminder's title, due date, or status",
parameters: z.object({
id: z.string().describe("The id of the reminder to update"),
title: z.string().nullable().describe("The new title for the reminder"),
dueDate: z.string().nullable().describe("The new due date for the reminder in ISO format"),
assignee: z.enum(users).nullable().describe("The new assignee for the reminder"),
status: z
.enum(["pending", "completed", "ignored"])
.nullable()
.describe("The new status for the reminder"),
}),
execute: async (args) => {
try {
const updatedReminder = await updateReminder(args.id, {
title: args.title || undefined,
dueDateString: args.dueDate || undefined,
status: args.status || undefined,
assignee: args.assignee || undefined,
})
return updatedReminder
} catch (error) {
console.error(`Tool "updateReminder" failed`, error)
return `toolcall failed. ${error}`
}
},
}),
tool({
name: "getTodoListNames",
description: "Get the names of all todo lists",
parameters: z.object({}),
execute: async () => {
try {
const lists = await kv.get("todos", {})
return Object.keys(lists)
} catch (error) {
console.error(`Tool "getTodoListNames" failed`, error)
return `toolcall failed. ${error}`
}
},
}),
tool({
name: "getTodoList",
description: "Get a todo list by name",
parameters: z.object({
name: z.string().describe("The name of the todo list to retrieve"),
}),
execute: async (args) => {
try {
const lists = await kv.get("todos", {})
const list = lists[args.name]
if (!list) {
return `Todo list "${args.name}" not found.`
}
return list
} catch (error) {
console.error(`Tool "getTodoList" failed for "${args.name}":`, error)
return `toolcall failed. ${error}`
}
},
}),
tool({
name: "updateTodoList",
description: "Update a todo list by name",
parameters: z.object({
name: z.string().describe("The name of the todo list to update"),
todoList: todoListSchema.describe("The todo list as a TodoList object"),
}),
execute: async (args) => {
try {
await kv.update("todos", {}, (todos) => {
todos[args.name] = todoListToString(args.todoList)
return todos
})
return `Todo list "${args.name}" updated successfully.`
} catch (error) {
console.error(`Tool "updateTodoList" failed for "${args.name}":`, error)
return `toolcall failed. ${error}`
}
},
}),
]
export const tools = []