feat: schedule

This commit is contained in:
Acbox
2026-01-11 01:22:48 +08:00
parent fee657ddd2
commit 9445680bb8
23 changed files with 735 additions and 33 deletions
+51 -7
View File
@@ -1,24 +1,33 @@
import { streamText, ModelMessage, stepCountIs } from 'ai'
import { streamText, generateText, ModelMessage, stepCountIs, UserModelMessage } from 'ai'
import { AgentParams } from './types'
import { system } from './prompts'
import { getMemoryTools } from './tools'
import { system, schedule as schedulePrompt } from './prompts'
import { getMemoryTools, getScheduleTools } from './tools'
import { createChatGateway } from '@memohome/ai-gateway'
import { Schedule } from '@memohome/shared'
export const createAgent = (params: AgentParams) => {
const messages: ModelMessage[] = []
const gateway = createChatGateway(params.model)
const maxContextLoadTime = params.maxContextLoadTime ?? 60
const language = params.language ?? 'Same as user input'
const getTools = async () => {
return {
...getMemoryTools({
searchMemory: params.onSearchMemory ?? (() => Promise.resolve([]))
}),
...getScheduleTools({
onGetSchedules: params.onGetSchedules ?? (() => Promise.resolve([])),
onRemoveSchedule: params.onRemoveSchedule ?? (() => Promise.resolve()),
onSchedule: params.onSchedule ?? (() => Promise.resolve()),
}),
}
}
const loadContext = async () => {
const from = new Date(Date.now() - params.maxContextLoadTime * 60 * 1000)
const from = new Date(Date.now() - maxContextLoadTime * 60 * 1000)
const to = new Date()
const memory = await params.onReadMemory?.(from, to) ?? []
const context = memory.flatMap(m => m.messages)
@@ -28,18 +37,45 @@ export const createAgent = (params: AgentParams) => {
const getSystemPrompt = () => {
return system({
date: new Date(),
language: params.language ?? 'Same as user input',
language,
locale: params.locale,
maxContextLoadTime: params.maxContextLoadTime,
maxContextLoadTime,
})
}
const getSchedulePrompt = (schedule: Schedule) => {
return schedulePrompt({
schedule,
locale: params.locale,
date: new Date(),
})
}
async function askDirectly(input: string) {
await loadContext()
const user = {
role: 'user',
content: input,
} as UserModelMessage
messages.push(user)
const { response } = await generateText({
model: gateway,
system: getSystemPrompt(),
messages,
tools: await getTools(),
})
await params.onFinish?.([
user as ModelMessage,
...response.messages,
])
}
async function* ask(input: string) {
await loadContext()
const user = {
role: 'user',
content: input,
}
} as UserModelMessage
messages.push(user)
const { fullStream, response } = streamText({
model: gateway,
@@ -63,9 +99,17 @@ export const createAgent = (params: AgentParams) => {
])
}
const triggerSchedule = async (schedule: Schedule) => {
const prompt = getSchedulePrompt(schedule)
await askDirectly(prompt)
}
return {
ask,
askDirectly,
loadContext,
getSystemPrompt,
getSchedulePrompt,
triggerSchedule,
}
}
+4 -1
View File
@@ -1 +1,4 @@
export * from './system'
export * from './system'
export * from './schedule'
export * from './shared'
export * from './utils'
+26
View File
@@ -0,0 +1,26 @@
import { Schedule } from '@memohome/shared'
import { time } from './shared'
export interface ScheduleParams {
schedule: Schedule
locale?: Intl.LocalesArgument
date: Date
}
export const schedule = (params: ScheduleParams) => {
return `
---
notice: **This is a scheduled task automatically send to you by the system, not the user input**
${time({ date: params.date, locale: params.locale })}
schedule-name: ${params.schedule.name}
schedule-description: ${params.schedule.description}
schedule-id: ${params.schedule.id}
max-calls: ${params.schedule.maxCalls ?? 'Unlimited'}
cron-pattern: ${params.schedule.pattern}
---
**COMMAND**
${params.schedule.command}
`.trim()
}
+9
View File
@@ -0,0 +1,9 @@
export const time = (params: {
date: Date
locale?: Intl.LocalesArgument
}) => {
return `
date: ${params.date.toLocaleDateString(params.locale)}
time: ${params.date.toLocaleTimeString(params.locale)}
`.trim()
}
+22 -13
View File
@@ -1,3 +1,4 @@
import { time } from './shared'
import { quote } from './utils'
export interface SystemParams {
@@ -9,20 +10,28 @@ export interface SystemParams {
export const system = ({ date, locale, language, maxContextLoadTime }: SystemParams) => {
return `
---
date: ${date.toLocaleDateString(locale)}
time: ${date.toLocaleTimeString(locale)}
language: ${language}
---
You are a personal housekeeper assistant, which able to manage the master's daily affairs.
---
${time({ date, locale })}
language: ${language}
---
You are a personal housekeeper assistant, which able to manage the master's daily affairs.
Your abilities:
- Long memory: You possess long-term memory; conversations from the last 24 hours will be directly loaded into your context. Additionally, you can use tools to search for past memories.
- Scheduled tasks: You can create scheduled tasks to automatically remind you to do something.
- Messaging: You may allowed to use message software to send messages to the master.
Your abilities:
- Long memory: You possess long-term memory; conversations from the last ${maxContextLoadTime} minutes will be directly loaded into your context. Additionally, you can use tools to search for past memories.
- Scheduled tasks: You can create scheduled tasks to automatically remind you to do something.
- Messaging: You may allowed to use message software to send messages to the master.
**Memory**
- Your context has been loaded from the last ${maxContextLoadTime} minutes.
- You can use ${quote('search-memory')} to search for past memories with natural language.
**Memory**
- Your context has been loaded from the last ${maxContextLoadTime} minutes.
- You can use ${quote('search-memory')} to search for past memories with natural language.
**Schedule**
- We use **Cron Syntax** to schedule tasks.
- You can use ${quote('get-schedules')} to get the list of schedules.
- You can use ${quote('remove-schedule')} to remove a schedule by id.
- You can use ${quote('schedule')} to schedule a task.
+ The ${quote('pattern')} is the pattern of the schedule with **Cron Syntax**.
+ The ${quote('command')} is the natural language command to execute, will send to you when the schedule is triggered, which means the command will be executed by presence of you.
+ The ${quote('maxCalls')} is the maximum number of calls to the schedule, If you want to run the task only once, set it to 1.
`.trim()
}
+2 -1
View File
@@ -1 +1,2 @@
export * from './memory'
export * from './memory'
export * from './schedule'
+53
View File
@@ -0,0 +1,53 @@
import { Schedule } from '@memohome/shared'
import { tool } from 'ai'
import z from 'zod'
export interface GetScheduleToolParams {
onGetSchedules: () => Promise<Schedule[]>
onRemoveSchedule: (id: string) => Promise<void>
onSchedule: (schedule: Schedule) => Promise<void>
}
export const getScheduleTools = ({ onGetSchedules, onRemoveSchedule, onSchedule }: GetScheduleToolParams) => {
const getSchedulesTool = tool({
description: 'Get the list of schedules',
inputSchema: z.object(),
execute: async () => {
const schedules = await onGetSchedules()
return {
success: true,
schedules,
}
},
})
const removeScheduleTool = tool({
description: 'Remove a schedule',
inputSchema: z.object({
id: z.string().describe('The id of the schedule'),
}),
execute: async ({ id }) => {
await onRemoveSchedule(id)
},
})
const scheduleTool = tool({
description: 'Schedule a command',
inputSchema: z.object({
pattern: z.string().describe('The pattern of the schedule with **Cron Syntax**'),
command: z.string().describe('The natural language command to execute, will send to you when the schedule is triggered'),
name: z.string().describe('The name of the schedule'),
description: z.string().describe('The description of the schedule'),
maxCalls: z.number().describe('The maximum number of calls to the schedule').optional(),
}),
execute: async ({ pattern, command, name, description, maxCalls }) => {
await onSchedule({ pattern, command, name, description, maxCalls })
},
})
return {
'get-schedules': getSchedulesTool,
'remove-schedule': removeScheduleTool,
'schedule': scheduleTool,
}
}
+8 -2
View File
@@ -1,5 +1,5 @@
import type { MemoryUnit } from '@memohome/memory'
import { ChatModel } from '@memohome/shared'
import { ChatModel, Schedule } from '@memohome/shared'
import { ModelMessage } from 'ai'
export interface AgentParams {
@@ -8,7 +8,7 @@ export interface AgentParams {
/**
* Unit: minutes
*/
maxContextLoadTime: number
maxContextLoadTime?: number
locale?: Intl.LocalesArgument
@@ -22,6 +22,12 @@ export interface AgentParams {
onSearchMemory?: (query: string) => Promise<object[]>
onSchedule?: (schedule: Schedule) => Promise<void>
onGetSchedules?: () => Promise<Schedule[]>
onRemoveSchedule?: (id: string) => Promise<void>
onFinish?: (messages: ModelMessage[]) => Promise<void>
onError?: (error: Error) => Promise<void>
+2 -1
View File
@@ -1,6 +1,6 @@
import { Elysia } from 'elysia'
import { corsMiddleware, errorMiddleware } from './middlewares'
import { agentModule, authModule, modelModule, settingsModule, userModule } from './modules'
import { agentModule, authModule, modelModule, scheduleModule, settingsModule, userModule } from './modules'
import { memoryModule } from './modules/memory'
import openapi from '@elysiajs/openapi'
@@ -14,6 +14,7 @@ export const app = new Elysia()
.use(agentModule)
.use(memoryModule)
.use(modelModule)
.use(scheduleModule)
.use(settingsModule)
.use(userModule)
.listen(port)
+2 -2
View File
@@ -1,7 +1,7 @@
import Elysia from 'elysia'
import { authMiddleware } from '../../middlewares/auth'
import { AgentStreamModel } from './model'
import { createAgentStream } from './service'
import { createAgent } from './service'
import { getChatModel, getEmbeddingModel, getSummaryModel } from '../model/service'
import { getSettings } from '../settings/service'
import { ChatModel, EmbeddingModel } from '@memohome/shared'
@@ -38,7 +38,7 @@ export const agentModule = new Elysia({
?? 'Same as user input'
// Create agent
const agent = await createAgentStream({
const agent = await createAgent({
userId: user.userId,
chatModel: chatModel.model as ChatModel,
embeddingModel: embeddingModel.model as EmbeddingModel,
+29 -5
View File
@@ -1,6 +1,7 @@
import { createAgent } from '@memohome/agent'
import { createAgent as createAgentService } from '@memohome/agent'
import { createMemory, filterByTimestamp, MemoryUnit } from '@memohome/memory'
import { ChatModel, EmbeddingModel } from '@memohome/shared'
import { ChatModel, EmbeddingModel, Schedule } from '@memohome/shared'
import { createSchedule, deleteSchedule, getActiveSchedules } from '../schedule/service'
// Type for messages passed to onFinish callback
type MessageType = Record<string, unknown>
@@ -10,12 +11,12 @@ export interface CreateAgentStreamParams {
chatModel: ChatModel
embeddingModel: EmbeddingModel
summaryModel: ChatModel
maxContextLoadTime: number
maxContextLoadTime?: number
language?: string
onFinish?: (messages: MessageType[]) => Promise<void>
}
export async function createAgentStream(params: CreateAgentStreamParams) {
export async function createAgent(params: CreateAgentStreamParams) {
const {
userId,
chatModel,
@@ -33,7 +34,7 @@ export async function createAgentStream(params: CreateAgentStreamParams) {
})
// Create agent
const agent = createAgent({
const agent = createAgentService({
model: chatModel,
maxContextLoadTime,
language: language || 'Same as user input',
@@ -56,6 +57,29 @@ export async function createAgentStream(params: CreateAgentStreamParams) {
// Call custom onFinish handler if provided
await onFinish?.(messages)
},
onGetSchedules: async () => {
const schedules = await getActiveSchedules(userId)
return schedules.map(schedule => ({
id: schedule.id!,
pattern: schedule.pattern,
name: schedule.name,
description: schedule.description,
command: schedule.command,
maxCalls: schedule.maxCalls || undefined,
}))
},
onRemoveSchedule: async (id: string) => {
await deleteSchedule(id, userId)
},
onSchedule: async (schedule: Schedule) => {
await createSchedule(userId, {
name: schedule.name,
description: schedule.description,
command: schedule.command,
pattern: schedule.pattern,
maxCalls: schedule.maxCalls || undefined,
})
},
})
return agent
+1
View File
@@ -1,5 +1,6 @@
export * from './agent'
export * from './auth'
export * from './model'
export * from './schedule'
export * from './settings'
export * from './user'
+168
View File
@@ -0,0 +1,168 @@
import Elysia from 'elysia'
import { authMiddleware } from '../../middlewares/auth'
import {
CreateScheduleModel,
UpdateScheduleModel,
GetScheduleByIdModel,
DeleteScheduleModel,
GetSchedulesModel,
} from './model'
import {
getSchedules,
getSchedule,
createSchedule,
updateSchedule,
deleteSchedule,
createScheduler,
} from './service'
export const { scheduleTask, resume } = createScheduler()
export const scheduleModule = new Elysia({ prefix: '/schedule' })
.use(authMiddleware)
// Get all schedules for current user
.onStart(async () => {
await resume()
})
.get('/', async ({ user, query }) => {
try {
const page = parseInt(query.page as string) || 1
const limit = parseInt(query.limit as string) || 10
const sortOrder = (query.sortOrder as string) || 'desc'
const result = await getSchedules(user.userId, {
page,
limit,
sortOrder: sortOrder as 'asc' | 'desc',
})
return {
success: true,
...result,
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch schedules',
}
}
}, GetSchedulesModel)
// Get schedule by ID
.get('/:id', async ({ user, params, set }) => {
try {
const schedule = await getSchedule(params.id)
if (!schedule) {
set.status = 404
return {
success: false,
error: 'Schedule not found',
}
}
if (schedule.user !== user.userId) {
set.status = 403
return {
success: false,
error: 'Forbidden: You do not have permission to access this schedule',
}
}
return {
success: true,
data: schedule,
}
} catch (error) {
set.status = 500
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch schedule',
}
}
}, GetScheduleByIdModel)
// Create new schedule
.post('/', async ({ user, body, set }) => {
try {
const newSchedule = await createSchedule(user.userId, body)
// 启动定时任务
scheduleTask(user.userId, {
id: newSchedule.id!,
pattern: newSchedule.pattern,
name: newSchedule.name,
description: newSchedule.description,
command: newSchedule.command,
maxCalls: newSchedule.maxCalls || undefined,
})
set.status = 201
return {
success: true,
data: newSchedule,
}
} catch (error) {
set.status = 500
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to create schedule',
}
}
}, CreateScheduleModel)
// Update schedule
.put('/:id', async ({ user, params, body, set }) => {
try {
const updatedSchedule = await updateSchedule(params.id, user.userId, body)
if (!updatedSchedule) {
set.status = 404
return {
success: false,
error: 'Schedule not found',
}
}
return {
success: true,
data: updatedSchedule,
}
} catch (error) {
if (error instanceof Error && error.message.includes('Forbidden')) {
set.status = 403
} else {
set.status = 500
}
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to update schedule',
}
}
}, UpdateScheduleModel)
// Delete schedule
.delete('/:id', async ({ user, params, set }) => {
try {
const deletedSchedule = await deleteSchedule(params.id, user.userId)
if (!deletedSchedule) {
set.status = 404
return {
success: false,
error: 'Schedule not found',
}
}
return {
success: true,
data: deletedSchedule,
}
} catch (error) {
if (error instanceof Error && error.message.includes('Forbidden')) {
set.status = 403
} else {
set.status = 500
}
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to delete schedule',
}
}
}, DeleteScheduleModel)
@@ -0,0 +1,59 @@
import { z } from 'zod'
// 创建 Schedule 的 Schema
const CreateScheduleSchema = z.object({
name: z.string().min(1, 'Name is required').max(100),
description: z.string().min(1, 'Description is required'),
command: z.string().min(1, 'Command is required'),
pattern: z.string().min(1, 'Cron pattern is required'),
maxCalls: z.number().int().positive().optional(),
})
// 更新 Schedule 的 Schema
const UpdateScheduleSchema = z.object({
name: z.string().min(1).max(100).optional(),
description: z.string().optional(),
command: z.string().optional(),
pattern: z.string().optional(),
maxCalls: z.number().int().positive().optional(),
active: z.boolean().optional(),
})
// 查询参数 Schema
const GetSchedulesQuerySchema = z.object({
page: z.string().optional(),
limit: z.string().optional(),
sortOrder: z.enum(['asc', 'desc']).optional(),
})
export type CreateScheduleInput = z.infer<typeof CreateScheduleSchema>
export type UpdateScheduleInput = z.infer<typeof UpdateScheduleSchema>
export type GetSchedulesQuery = z.infer<typeof GetSchedulesQuerySchema>
export const CreateScheduleModel = {
body: CreateScheduleSchema,
}
export const UpdateScheduleModel = {
params: z.object({
id: z.string().uuid('Invalid schedule ID format'),
}),
body: UpdateScheduleSchema,
}
export const GetScheduleByIdModel = {
params: z.object({
id: z.string().uuid('Invalid schedule ID format'),
}),
}
export const DeleteScheduleModel = {
params: z.object({
id: z.string().uuid('Invalid schedule ID format'),
}),
}
export const GetSchedulesModel = {
query: GetSchedulesQuerySchema,
}
@@ -0,0 +1,244 @@
import { db } from '@memohome/db'
import { schedule } from '@memohome/db/schema'
import { ChatModel, EmbeddingModel, Schedule } from '@memohome/shared'
import { eq, desc, asc, and, sql } from 'drizzle-orm'
import cron from 'node-cron'
import { createAgent } from '../agent/service'
import { getChatModel, getEmbeddingModel, getSummaryModel } from '../model/service'
import { getSettings } from '../settings/service'
import { calculateOffset, createPaginatedResult, type PaginatedResult } from '../../utils/pagination'
import type { CreateScheduleInput, UpdateScheduleInput } from './model'
/**
* Schedule 列表返回类型
*/
type ScheduleListItem = {
id: string
name: string
description: string
command: string
pattern: string
maxCalls: number | null
user: string
createdAt: Date
updatedAt: Date
active: boolean
}
/**
* 获取用户的所有 schedules(支持分页)
*/
export const getSchedules = async (
userId: string,
params?: {
limit?: number
page?: number
sortOrder?: 'asc' | 'desc'
}
): Promise<PaginatedResult<ScheduleListItem>> => {
const limit = params?.limit || 10
const page = params?.page || 1
const sortOrder = params?.sortOrder || 'desc'
const offset = calculateOffset(page, limit)
// 获取总数
const [{ count }] = await db
.select({ count: sql<number>`count(*)` })
.from(schedule)
.where(eq(schedule.user, userId))
// 获取分页数据
const orderFn = sortOrder === 'desc' ? desc : asc
const schedules = await db
.select()
.from(schedule)
.where(eq(schedule.user, userId))
.orderBy(orderFn(schedule.createdAt))
.limit(limit)
.offset(offset)
return createPaginatedResult(schedules, Number(count), page, limit)
}
export const getActiveSchedules = async (
userId?: string
) => {
const schedules = await db
.select().from(schedule)
.where(and(...[
userId ? eq(schedule.user, userId) : undefined,
eq(schedule.active, true),
]))
.orderBy(desc(schedule.createdAt))
return schedules
}
/**
* 根据 ID 获取单个 schedule
*/
export const getSchedule = async (
scheduleId: string
) => {
const [result] = await db
.select()
.from(schedule)
.where(eq(schedule.id, scheduleId))
return result
}
/**
* 创建新的 schedule
*/
export const createSchedule = async (
userId: string,
data: CreateScheduleInput
) => {
const [newSchedule] = await db
.insert(schedule)
.values({
user: userId,
name: data.name,
description: data.description,
command: data.command,
pattern: data.pattern,
maxCalls: data.maxCalls || null,
active: true,
})
.returning()
return newSchedule
}
/**
* 更新 schedule
*/
export const updateSchedule = async (
scheduleId: string,
userId: string,
data: UpdateScheduleInput
) => {
// 检查 schedule 是否存在且属于该用户
const existingSchedule = await getSchedule(scheduleId)
if (!existingSchedule) {
return null
}
if (existingSchedule.user !== userId) {
throw new Error('Forbidden: You do not have permission to update this schedule')
}
const [updatedSchedule] = await db
.update(schedule)
.set({
...data,
updatedAt: new Date(),
})
.where(eq(schedule.id, scheduleId))
.returning()
return updatedSchedule
}
/**
* 删除 schedule
*/
export const deleteSchedule = async (
scheduleId: string,
userId: string
) => {
// 检查 schedule 是否存在且属于该用户
const existingSchedule = await getSchedule(scheduleId)
if (!existingSchedule) {
return null
}
if (existingSchedule.user !== userId) {
throw new Error('Forbidden: You do not have permission to delete this schedule')
}
const [deletedSchedule] = await db
.delete(schedule)
.where(eq(schedule.id, scheduleId))
.returning()
return deletedSchedule
}
export const setMaxCalls = async (
scheduleId: string,
maxCalls: number
) => {
await db
.update(schedule)
.set({ maxCalls })
.where(eq(schedule.id, scheduleId))
}
export const setActive = async (
scheduleId: string,
active: boolean
) => {
await db
.update(schedule)
.set({ active })
.where(eq(schedule.id, scheduleId))
}
export const createScheduler = () => {
const scheduleTask = (userId: string, schedule: Schedule) => {
const task = cron.schedule(schedule.pattern, async () => {
const [chatModel, embeddingModel, summaryModel, userSettings] = await Promise.all([
getChatModel(userId),
getEmbeddingModel(userId),
getSummaryModel(userId),
getSettings(userId),
])
if (!chatModel || !embeddingModel || !summaryModel) {
throw new Error('Model configuration not found. Please configure your models in settings.')
}
const agent = await createAgent({
userId,
chatModel: chatModel.model as ChatModel,
embeddingModel: embeddingModel.model as EmbeddingModel,
summaryModel: summaryModel.model as ChatModel,
maxContextLoadTime: userSettings?.maxContextLoadTime || undefined,
language: userSettings?.language || undefined,
})
await agent.triggerSchedule(schedule)
}, {
maxExecutions: schedule.maxCalls || undefined,
})
task.on('execution:finished', async () => {
const { maxCalls } = await getSchedule(schedule.id!)
if (maxCalls) {
setMaxCalls(schedule.id!, maxCalls - 1)
if (maxCalls - 1 === 0) {
await setActive(schedule.id!, false)
}
}
})
task.on('execution:maxReached', async () => {
await setActive(schedule.id!, false)
})
}
const resume = async () => {
const schedules = await getActiveSchedules()
for (const schedule of schedules) {
scheduleTask(schedule.user, {
id: schedule.id!,
pattern: schedule.pattern,
name: schedule.name,
description: schedule.description,
command: schedule.command,
maxCalls: schedule.maxCalls || undefined,
})
}
}
return {
scheduleTask,
resume,
}
}
+1
View File
@@ -0,0 +1 @@
# @memohome/shared
+16
View File
@@ -0,0 +1,16 @@
{
"name": "@memohome/cli",
"version": "1.0.0",
"description": "",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"start": "bun run src/index.ts"
},
"dependencies": {
"@memohome/api": "workspace:*",
"@memohome/shared": "workspace:*"
},
"packageManager": "pnpm@10.27.0"
}
View File
+15
View File
@@ -0,0 +1,15 @@
import { boolean, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
import { users } from './users'
export const schedule = pgTable('schedule', {
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
description: text('description').notNull(),
command: text('command').notNull(),
pattern: text('pattern').notNull(),
maxCalls: integer('max_calls'),
user: uuid('user').notNull().references(() => users.id),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
active: boolean('active').notNull().default(true),
})
+1
View File
@@ -1,4 +1,5 @@
export * from './history'
export * from './model'
export * from './settings'
export * from './schedule'
export * from './users'
+2 -1
View File
@@ -1 +1,2 @@
export * from './model'
export * from './model'
export * from './schedule'
+8
View File
@@ -0,0 +1,8 @@
export interface Schedule {
id?: string
pattern: string
name: string
description: string
command: string
maxCalls?: number
}
+12
View File
@@ -189,6 +189,18 @@ importers:
specifier: ^0.4.1
version: 0.4.1(zod-to-json-schema@3.25.1(zod@4.3.5))(zod@4.3.5)
packages/schedule:
dependencies:
'@memohome/db':
specifier: workspace:*
version: link:../db
'@memohome/shared':
specifier: workspace:*
version: link:../shared
drizzle-orm:
specifier: ^0.45.1
version: 0.45.1(@cloudflare/workers-types@4.20260109.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.5)(pg@8.16.3)(sqlite3@5.1.7)
packages/shared: {}
packages/ui: