#!/usr/bin/env node import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url); // mindspace-sandbox-mcp.mjs import path2 from "node:path"; import fs2 from "node:fs"; import readline from "node:readline"; import { execFileSync } from "node:child_process"; import mysql from "mysql2/promise"; // schedule-service.mjs import crypto from "node:crypto"; // schedule-time.mjs var DEFAULT_TIMEZONE = "Asia/Shanghai"; function pad2(value) { return String(value).padStart(2, "0"); } function normalizeTimezone(timezone) { return String(timezone || process.env.H5_DEFAULT_TIMEZONE || DEFAULT_TIMEZONE).trim() || DEFAULT_TIMEZONE; } function getLocalParts(epochMs = Date.now(), timezone = DEFAULT_TIMEZONE) { const formatter = new Intl.DateTimeFormat("en-CA", { timeZone: normalizeTimezone(timezone), year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false }); const parts = Object.fromEntries( formatter.formatToParts(new Date(epochMs)).map((part) => [part.type, part.value]) ); return { year: Number(parts.year), month: Number(parts.month), day: Number(parts.day), hour: Number(parts.hour === "24" ? 0 : parts.hour), minute: Number(parts.minute), second: Number(parts.second) }; } function localDateLabel(epochMs = Date.now(), timezone = DEFAULT_TIMEZONE) { const parts = getLocalParts(epochMs, timezone); return `${parts.month}\u6708${parts.day}\u65E5`; } function startOfLocalDay(epochMs = Date.now(), timezone = DEFAULT_TIMEZONE) { const parts = getLocalParts(epochMs, timezone); return zonedTimeToEpochMs( { year: parts.year, month: parts.month, day: parts.day, hour: 0, minute: 0, second: 0 }, timezone ); } function addLocalDays(epochMs, days, timezone = DEFAULT_TIMEZONE) { const parts = getLocalParts(epochMs, timezone); const utc = Date.UTC(parts.year, parts.month - 1, parts.day + Number(days || 0), parts.hour, parts.minute, parts.second); const shifted = new Date(utc); return zonedTimeToEpochMs( { year: shifted.getUTCFullYear(), month: shifted.getUTCMonth() + 1, day: shifted.getUTCDate(), hour: shifted.getUTCHours(), minute: shifted.getUTCMinutes(), second: shifted.getUTCSeconds() }, timezone ); } function zonedTimeToEpochMs(parts, timezone = DEFAULT_TIMEZONE) { const tz = normalizeTimezone(timezone); let guess = Date.UTC( Number(parts.year), Number(parts.month) - 1, Number(parts.day), Number(parts.hour ?? 0), Number(parts.minute ?? 0), Number(parts.second ?? 0), 0 ); for (let i = 0; i < 3; i += 1) { const actual = getLocalParts(guess, tz); const actualAsUtc = Date.UTC( actual.year, actual.month - 1, actual.day, actual.hour, actual.minute, actual.second, 0 ); const desiredAsUtc = Date.UTC( Number(parts.year), Number(parts.month) - 1, Number(parts.day), Number(parts.hour ?? 0), Number(parts.minute ?? 0), Number(parts.second ?? 0), 0 ); const delta = actualAsUtc - desiredAsUtc; if (delta === 0) break; guess -= delta; } return guess; } function nextDailyRunAt({ hour, minute = 0, timezone = DEFAULT_TIMEZONE, now = Date.now() }) { const tz = normalizeTimezone(timezone); const todayStart = startOfLocalDay(now, tz); const todayParts = getLocalParts(todayStart, tz); let runAt = zonedTimeToEpochMs( { year: todayParts.year, month: todayParts.month, day: todayParts.day, hour: Number(hour), minute: Number(minute), second: 0 }, tz ); if (runAt <= now) { const tomorrowStart = addLocalDays(todayStart, 1, tz); const tomorrowParts = getLocalParts(tomorrowStart, tz); runAt = zonedTimeToEpochMs( { year: tomorrowParts.year, month: tomorrowParts.month, day: tomorrowParts.day, hour: Number(hour), minute: Number(minute), second: 0 }, tz ); } return runAt; } function formatLocalTime(epochMs, timezone = DEFAULT_TIMEZONE) { const parts = getLocalParts(epochMs, timezone); return `${pad2(parts.hour)}:${pad2(parts.minute)}`; } function parseLocalDateTimeString(value, timezone = DEFAULT_TIMEZONE) { const raw = String(value ?? "").trim(); if (!raw) return null; const match = raw.match( /^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/ ); if (!match) { throw new Error(`\u65E0\u6CD5\u89E3\u6790\u672C\u5730\u65F6\u95F4\u300C${raw}\u300D\uFF0C\u8BF7\u4F7F\u7528 YYYY-MM-DD HH:mm`); } return zonedTimeToEpochMs( { year: Number(match[1]), month: Number(match[2]), day: Number(match[3]), hour: Number(match[4] ?? 0), minute: Number(match[5] ?? 0), second: Number(match[6] ?? 0) }, timezone ); } function resolveScheduleTimestamp({ epochMs = null, localString = null, timezone = DEFAULT_TIMEZONE, fieldName = "\u65F6\u95F4", now = Date.now() } = {}) { const local = String(localString ?? "").trim(); if (local) return parseLocalDateTimeString(local, timezone); if (epochMs == null || epochMs === "") return null; return assertReasonableScheduleEpoch(epochMs, { now, fieldName }); } function assertReasonableScheduleEpoch(epochMs, { now = Date.now(), fieldName = "\u65F6\u95F4", maxPastDays = 2, maxFutureDays = 400 } = {}) { const safe = Number(epochMs); if (!Number.isFinite(safe) || safe <= 0) { throw new Error(`${fieldName}\u65E0\u6548\uFF0C\u8BF7\u6539\u7528 YYYY-MM-DD HH:mm \u7684 local \u5B57\u6BB5`); } const min = now - maxPastDays * 864e5; const max = now + maxFutureDays * 864e5; if (safe < min || safe > max) { const label = new Intl.DateTimeFormat("zh-CN", { timeZone: normalizeTimezone(process.env.H5_DEFAULT_TIMEZONE), dateStyle: "short", timeStyle: "short" }).format(new Date(safe)); throw new Error( `${fieldName}\uFF08${label}\uFF09\u8D85\u51FA\u5408\u7406\u8303\u56F4\u3002\u8BF7\u6539\u7528 startLocal/remindLocal\uFF08YYYY-MM-DD HH:mm\uFF0C\u7528\u6237\u65F6\u533A\uFF09\uFF0C\u7981\u6B62\u81EA\u884C\u4F30\u7B97 Unix \u6BEB\u79D2\u3002` ); } return safe; } // schedule-service.mjs var DEFAULT_TIMEZONE2 = "Asia/Shanghai"; function nowMs() { return Date.now(); } function rowToItem(row) { if (!row) return null; return { id: row.id, userId: row.user_id, kind: row.kind, title: row.title, description: row.description ?? null, status: row.status, startAt: row.start_at == null ? null : Number(row.start_at), endAt: row.end_at == null ? null : Number(row.end_at), dueAt: row.due_at == null ? null : Number(row.due_at), allDay: Boolean(row.all_day), timezone: row.timezone || DEFAULT_TIMEZONE2, location: row.location ?? null, createdAt: Number(row.created_at), updatedAt: Number(row.updated_at) }; } function rowToDigest(row) { if (!row) return null; return { id: row.id, userId: row.user_id, hour: Number(row.hour), minute: Number(row.minute), timezone: row.timezone || DEFAULT_TIMEZONE2, channel: row.channel || "wechat", status: row.status, nextRunAt: Number(row.next_run_at), lastRunAt: row.last_run_at == null ? null : Number(row.last_run_at), attempts: Number(row.attempts ?? 0) }; } function rowToBalanceAlert(row) { if (!row) return null; return { id: row.id, userId: row.user_id, thresholdCents: Number(row.threshold_cents), channel: row.channel || "wechat", status: row.status, nextRunAt: Number(row.next_run_at), lastRunAt: row.last_run_at == null ? null : Number(row.last_run_at), lastNotifiedBalanceCents: row.last_notified_balance_cents == null ? null : Number(row.last_notified_balance_cents), attempts: Number(row.attempts ?? 0), lastError: row.last_error ?? null, lockedUntil: row.locked_until == null ? null : Number(row.locked_until), sourceChannel: row.source_channel ?? null, sourceSessionId: row.source_session_id ?? null, sourceMessageId: row.source_message_id ?? null, sourceText: row.source_text ?? null }; } function rowToReminder(row) { if (!row) return null; return { id: row.id, userId: row.user_id, itemId: row.item_id, remindAt: Number(row.remind_at), offsetMinutes: row.offset_minutes == null ? null : Number(row.offset_minutes), channel: row.channel || "wechat", status: row.status, attempts: Number(row.attempts ?? 0), lastError: row.last_error ?? null, lockedUntil: row.locked_until == null ? null : Number(row.locked_until), sentAt: row.sent_at == null ? null : Number(row.sent_at), createdAt: Number(row.created_at), updatedAt: Number(row.updated_at) }; } function rowToReminderWithItem(row) { const reminder = rowToReminder(row); if (!reminder) return null; return { ...reminder, itemTitle: String(row.item_title ?? "").trim(), itemKind: row.item_kind === "event" ? "event" : "task", itemTimezone: row.item_timezone || DEFAULT_TIMEZONE2, itemStartAt: row.item_start_at == null ? null : Number(row.item_start_at), itemEndAt: row.item_end_at == null ? null : Number(row.item_end_at) }; } function parseJsonColumn(value) { if (value == null || value === "") return null; if (typeof value === "string") { try { return JSON.parse(value); } catch { return null; } } if (typeof value === "object") return value; return null; } function rowToUserNotification(row) { if (!row) return null; return { id: row.id, userId: row.user_id, channel: row.channel, notificationType: row.notification_type, title: row.title, body: row.body, data: parseJsonColumn(row.data_json), status: row.status, readAt: row.read_at == null ? null : Number(row.read_at), createdAt: Number(row.created_at), updatedAt: Number(row.updated_at) }; } function formatTodoDigest(items, { now = nowMs(), timezone = DEFAULT_TIMEZONE2 } = {}) { const date = localDateLabel(now, timezone); if (!items.length) { return `${date} \u5F85\u529E\u8BB0\u5F55 \u4ECA\u5929\u6682\u65F6\u6CA1\u6709\u5F85\u529E\u3002`; } const lines = [`${date} \u5F85\u529E\u8BB0\u5F55`, ""]; items.forEach((item, index) => { const time = item.dueAt || item.startAt ? `\uFF08${formatLocalTime(item.dueAt || item.startAt, timezone)}\uFF09` : ""; lines.push(`${index + 1}. ${item.title}${time}`); }); return lines.join("\n"); } function createScheduleService(pool, options = {}) { const defaultTimezone = normalizeTimezone(options.defaultTimezone || process.env.H5_DEFAULT_TIMEZONE); const clock = options.clock || { now: nowMs }; const createItem = async ({ userId, kind = "task", title, description = null, startAt = null, endAt = null, dueAt = null, allDay = false, timezone = defaultTimezone, location = null, sourceChannel = "agent", sourceSessionId = null, sourceMessageId = null, sourceText = null, metadata = null }) => { if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237"); const cleanTitle = String(title ?? "").trim(); if (!cleanTitle) throw new Error("\u7F3A\u5C11\u5F85\u529E\u6807\u9898"); const safeKind = kind === "event" ? "event" : "task"; const id = crypto.randomUUID(); const ts = clock.now(); await pool.query( `INSERT INTO h5_schedule_items (id, user_id, kind, title, description, status, start_at, end_at, due_at, all_day, timezone, location, source_channel, source_session_id, source_message_id, source_text, metadata_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ id, userId, safeKind, cleanTitle, description, startAt, endAt, dueAt, allDay ? 1 : 0, normalizeTimezone(timezone), location, sourceChannel, sourceSessionId, sourceMessageId, sourceText, metadata ? JSON.stringify(metadata) : null, ts, ts ] ); return { id, userId, kind: safeKind, title: cleanTitle, startAt, endAt, dueAt, allDay: Boolean(allDay), timezone: normalizeTimezone(timezone), status: "active", createdAt: ts, updatedAt: ts }; }; const listItems = async ({ userId, from = null, to = null, status = "active", limit = 100 } = {}) => { if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237"); const clauses = ["user_id = ?", "deleted_at IS NULL"]; const params = [userId]; if (status) { clauses.push("status = ?"); params.push(status); } if (from != null && to != null) { clauses.push( `( (start_at IS NOT NULL AND start_at >= ? AND start_at < ?) OR (due_at IS NOT NULL AND due_at >= ? AND due_at < ?) OR (start_at IS NULL AND due_at IS NULL) )` ); params.push(from, to, from, to); } params.push(Math.max(1, Math.min(500, Number(limit) || 100))); const [rows] = await pool.query( `SELECT * FROM h5_schedule_items WHERE ${clauses.join(" AND ")} ORDER BY COALESCE(start_at, due_at, 9223372036854775807), created_at LIMIT ?`, params ); return rows.map(rowToItem); }; const listItemsBySourceMessage = async ({ userId, sourceMessageId, limit = 20 } = {}) => { if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237"); const cleanSourceMessageId = String(sourceMessageId ?? "").trim(); if (!cleanSourceMessageId) return []; const [rows] = await pool.query( `SELECT * FROM h5_schedule_items WHERE user_id = ? AND source_message_id = ? AND deleted_at IS NULL ORDER BY created_at DESC LIMIT ?`, [userId, cleanSourceMessageId, Math.max(1, Math.min(100, Number(limit) || 20))] ); return rows.map(rowToItem); }; const listTodayTodoItems = async ({ userId, timezone = defaultTimezone, now = clock.now() } = {}) => { const start = startOfLocalDay(now, timezone); const end = addLocalDays(start, 1, timezone); return listItems({ userId, from: start, to: end, status: "active", limit: 200 }); }; const listUpcomingItems = async ({ userId, timezone = defaultTimezone, days = 7, now = clock.now() } = {}) => { const safeDays = Math.max(1, Math.min(30, Number(days) || 7)); const start = startOfLocalDay(now, timezone); const end = addLocalDays(start, safeDays, timezone); return listItems({ userId, from: start, to: end, status: "active", limit: 200 }); }; const listUpcomingReminders = async ({ userId, timezone = defaultTimezone, days = 7, now = clock.now(), limit = 100 } = {}) => { if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237"); const safeDays = Math.max(1, Math.min(30, Number(days) || 7)); const start = startOfLocalDay(now, timezone); const end = addLocalDays(start, safeDays, timezone); const [rows] = await pool.query( `SELECT r.*, i.title AS item_title, i.kind AS item_kind, i.timezone AS item_timezone, i.start_at AS item_start_at, i.end_at AS item_end_at FROM h5_schedule_reminders r INNER JOIN h5_schedule_items i ON i.id = r.item_id WHERE r.user_id = ? AND r.status IN ('pending', 'locked', 'sent') AND r.remind_at >= ? AND r.remind_at < ? AND i.deleted_at IS NULL AND i.status = 'active' ORDER BY r.remind_at ASC LIMIT ?`, [ userId, start, end, Math.max(1, Math.min(200, Number(limit) || 100)) ] ); return rows.map(rowToReminderWithItem); }; const getItem = async ({ userId, itemId }) => { if (!userId || !itemId) throw new Error("\u7F3A\u5C11\u4E8B\u9879\u53C2\u6570"); const [rows] = await pool.query( `SELECT * FROM h5_schedule_items WHERE id = ? AND user_id = ? AND deleted_at IS NULL LIMIT 1`, [itemId, userId] ); return rowToItem(rows[0]); }; const getReminder = async ({ userId, reminderId }) => { if (!userId || !reminderId) throw new Error("\u7F3A\u5C11\u63D0\u9192\u53C2\u6570"); const [rows] = await pool.query( `SELECT r.*, i.title AS item_title, i.kind AS item_kind, i.timezone AS item_timezone, i.start_at AS item_start_at, i.end_at AS item_end_at FROM h5_schedule_reminders r INNER JOIN h5_schedule_items i ON i.id = r.item_id WHERE r.id = ? AND r.user_id = ? AND i.deleted_at IS NULL LIMIT 1`, [reminderId, userId] ); return rowToReminderWithItem(rows[0]); }; const cancelReminder = async ({ userId, reminderId, reason = "\u7528\u6237\u5FFD\u7565" } = {}) => { const reminder = await getReminder({ userId, reminderId }); if (!reminder) throw new Error("\u63D0\u9192\u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE"); if (reminder.status === "cancelled") return reminder; if (reminder.status === "sent") throw new Error("\u5DF2\u901A\u77E5\u7684\u63D0\u9192\u4E0D\u80FD\u5FFD\u7565"); return markReminderCancelled(reminder, reason); }; const deleteReminder = async ({ userId, reminderId }) => { if (!userId || !reminderId) throw new Error("\u7F3A\u5C11\u63D0\u9192\u53C2\u6570"); const [result] = await pool.query( `DELETE FROM h5_schedule_reminders WHERE id = ? AND user_id = ?`, [reminderId, userId] ); return Number(result?.affectedRows ?? 0) > 0; }; const deleteReminders = async ({ userId, reminderIds }) => { if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237"); const ids = [...new Set((reminderIds ?? []).map((id) => String(id ?? "").trim()).filter(Boolean))]; if (ids.length === 0) return 0; const [result] = await pool.query( `DELETE FROM h5_schedule_reminders WHERE user_id = ? AND id IN (${ids.map(() => "?").join(", ")})`, [userId, ...ids] ); return Number(result?.affectedRows ?? 0); }; const createReminder = async ({ userId, itemId, remindAt, offsetMinutes = null, channel = "wechat" }) => { if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237"); if (!itemId) throw new Error("\u7F3A\u5C11\u4E8B\u9879"); const safeRemindAt = Number(remindAt); if (!Number.isFinite(safeRemindAt) || safeRemindAt <= 0) { throw new Error("\u63D0\u9192\u65F6\u95F4\u65E0\u6548"); } const safeOffsetMinutes = offsetMinutes == null || offsetMinutes === "" ? null : Number(offsetMinutes); if (safeOffsetMinutes != null && !Number.isInteger(safeOffsetMinutes)) { throw new Error("\u63D0\u9192\u504F\u79FB\u5206\u949F\u65E0\u6548"); } const safeChannel = channel === "in_app" ? "in_app" : "wechat"; const [itemRows] = await pool.query( `SELECT id FROM h5_schedule_items WHERE id = ? AND user_id = ? AND deleted_at IS NULL LIMIT 1`, [itemId, userId] ); if (!itemRows[0]) throw new Error("\u4E8B\u9879\u4E0D\u5B58\u5728\u6216\u65E0\u6743\u8BBF\u95EE"); const id = crypto.randomUUID(); const ts = clock.now(); await pool.query( `INSERT INTO h5_schedule_reminders (id, user_id, item_id, remind_at, offset_minutes, channel, status, attempts, last_error, locked_until, sent_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, 'pending', 0, NULL, NULL, NULL, ?, ?) ON DUPLICATE KEY UPDATE offset_minutes = VALUES(offset_minutes), status = 'pending', last_error = NULL, locked_until = NULL, updated_at = VALUES(updated_at)`, [id, userId, itemId, safeRemindAt, safeOffsetMinutes, safeChannel, ts, ts] ); const [rows] = await pool.query( `SELECT * FROM h5_schedule_reminders WHERE item_id = ? AND remind_at = ? AND channel = ? LIMIT 1`, [itemId, safeRemindAt, safeChannel] ); return rowToReminder(rows[0]) || { id, userId, itemId, remindAt: safeRemindAt, offsetMinutes: safeOffsetMinutes, channel: safeChannel, status: "pending", attempts: 0, lastError: null, lockedUntil: null, sentAt: null, createdAt: ts, updatedAt: ts }; }; const listDigestSubscriptions = async ({ userId, digestType = "todo_day", channel = null, status = null, limit = 20 } = {}) => { if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237"); const clauses = ["user_id = ?", "digest_type = ?"]; const params = [userId, digestType]; if (channel) { clauses.push("channel = ?"); params.push(channel); } if (status) { const statuses = Array.isArray(status) ? status : [status]; clauses.push(`status IN (${statuses.map(() => "?").join(", ")})`); params.push(...statuses); } params.push(Math.max(1, Math.min(200, Number(limit) || 20))); const [rows] = await pool.query( `SELECT * FROM h5_schedule_digest_subscriptions WHERE ${clauses.join(" AND ")} ORDER BY next_run_at ASC, created_at DESC LIMIT ?`, params ); return rows.map(rowToDigest); }; const createDailyTodoDigest = async ({ userId, hour, minute = 0, timezone = defaultTimezone, channel = "wechat", sourceChannel = "agent", sourceSessionId = null, sourceMessageId = null, sourceText = null }) => { if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237"); const safeHour = Number(hour); const safeMinute = Number(minute); if (!Number.isInteger(safeHour) || safeHour < 0 || safeHour > 23) { throw new Error("\u63D0\u9192\u5C0F\u65F6\u65E0\u6548"); } if (!Number.isInteger(safeMinute) || safeMinute < 0 || safeMinute > 59) { throw new Error("\u63D0\u9192\u5206\u949F\u65E0\u6548"); } const tz = normalizeTimezone(timezone); const nextRunAt = nextDailyRunAt({ hour: safeHour, minute: safeMinute, timezone: tz, now: clock.now() }); const id = crypto.randomUUID(); const ts = clock.now(); await pool.query( `INSERT INTO h5_schedule_digest_subscriptions (id, user_id, digest_type, hour, minute, timezone, channel, status, next_run_at, source_channel, source_session_id, source_message_id, source_text, created_at, updated_at) VALUES (?, ?, 'todo_day', ?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE hour = VALUES(hour), minute = VALUES(minute), timezone = VALUES(timezone), channel = VALUES(channel), status = 'active', next_run_at = VALUES(next_run_at), source_channel = VALUES(source_channel), source_session_id = VALUES(source_session_id), source_message_id = VALUES(source_message_id), source_text = VALUES(source_text), updated_at = VALUES(updated_at)`, [ id, userId, safeHour, safeMinute, tz, channel, nextRunAt, sourceChannel, sourceSessionId, sourceMessageId, sourceText, ts, ts ] ); const [rows] = await pool.query( `SELECT * FROM h5_schedule_digest_subscriptions WHERE user_id = ? AND digest_type = 'todo_day' AND channel = ? LIMIT 1`, [userId, channel] ); return rowToDigest(rows[0]) || { id, userId, hour: safeHour, minute: safeMinute, timezone: tz, channel, status: "active", nextRunAt, lastRunAt: null, attempts: 0 }; }; const createBalanceLowAlert = async ({ userId, thresholdCents, channel = "wechat", sourceChannel = "agent", sourceSessionId = null, sourceMessageId = null, sourceText = null } = {}) => { if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237"); const safeThreshold = Number(thresholdCents); if (!Number.isFinite(safeThreshold) || safeThreshold < 0) { throw new Error("\u4F59\u989D\u9608\u503C\u65E0\u6548"); } const now = clock.now(); const id = crypto.randomUUID(); await pool.query( `INSERT INTO h5_balance_alert_subscriptions (id, user_id, threshold_cents, channel, status, next_run_at, last_run_at, last_notified_balance_cents, attempts, locked_until, last_error, source_channel, source_session_id, source_message_id, source_text, created_at, updated_at) VALUES (?, ?, ?, ?, 'active', ?, NULL, NULL, 0, NULL, NULL, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE threshold_cents = VALUES(threshold_cents), channel = VALUES(channel), status = 'active', next_run_at = VALUES(next_run_at), last_error = NULL, locked_until = NULL, updated_at = VALUES(updated_at)`, [id, userId, safeThreshold, channel, now, sourceChannel, sourceSessionId, sourceMessageId, sourceText, now, now] ); const [rows] = await pool.query( `SELECT * FROM h5_balance_alert_subscriptions WHERE user_id = ? AND channel = ? LIMIT 1`, [userId, channel] ); return rowToBalanceAlert(rows[0]) || { id, userId, thresholdCents: safeThreshold, channel, status: "active", nextRunAt: now, lastRunAt: null, lastNotifiedBalanceCents: null, attempts: 0 }; }; const listDueReminders = async ({ now = clock.now(), limit = 50 } = {}) => { const [rows] = await pool.query( `SELECT r.* FROM h5_schedule_reminders r INNER JOIN h5_schedule_items i ON i.id = r.item_id WHERE r.status = 'pending' AND r.remind_at <= ? AND (r.locked_until IS NULL OR r.locked_until <= ?) AND i.deleted_at IS NULL AND i.status = 'active' ORDER BY r.remind_at ASC LIMIT ?`, [now, now, Math.max(1, Math.min(200, Number(limit) || 50))] ); return rows.map(rowToReminder); }; const lockReminder = async (id, { now = clock.now(), lockMs = 12e4 } = {}) => { const lockedUntil = now + lockMs; const [result] = await pool.query( `UPDATE h5_schedule_reminders SET status = 'locked', locked_until = ?, attempts = attempts + 1, updated_at = ? WHERE id = ? AND status = 'pending' AND remind_at <= ?`, [lockedUntil, now, id, now] ); if (Number(result?.affectedRows ?? 0) !== 1) return null; const [rows] = await pool.query( `SELECT * FROM h5_schedule_reminders WHERE id = ? LIMIT 1`, [id] ); return rowToReminder(rows[0]); }; const markReminderSent = async (reminder, { now = clock.now() } = {}) => { await pool.query( `UPDATE h5_schedule_reminders SET status = 'sent', sent_at = ?, locked_until = NULL, last_error = NULL, updated_at = ? WHERE id = ?`, [now, now, reminder.id] ); return { ...reminder, status: "sent", sentAt: now, lockedUntil: null }; }; const markReminderCancelled = async (reminder, reason, { now = clock.now() } = {}) => { const lastError = String(reason ?? "\u5DF2\u53D6\u6D88").slice(0, 500); await pool.query( `UPDATE h5_schedule_reminders SET status = 'cancelled', locked_until = NULL, last_error = ?, updated_at = ? WHERE id = ?`, [lastError, now, reminder.id] ); return { ...reminder, status: "cancelled", lastError }; }; const markReminderFailed = async (reminder, error, { now = clock.now(), retryMs = 10 * 60 * 1e3, maxAttempts = 5 } = {}) => { const attempts = Number(reminder.attempts ?? 0); const status = attempts >= maxAttempts ? "failed" : "pending"; const lockedUntil = status === "pending" ? now + retryMs : null; await pool.query( `UPDATE h5_schedule_reminders SET status = ?, locked_until = ?, last_error = ?, updated_at = ? WHERE id = ?`, [ status, lockedUntil, String(error?.message ?? error ?? "\u53D1\u9001\u5931\u8D25").slice(0, 500), now, reminder.id ] ); }; const buildReminderText = async (reminder) => { const item = await getItem({ userId: reminder.userId, itemId: reminder.itemId }); if (!item || item.status !== "active") return null; const timezone = item.timezone || defaultTimezone; const remindLabel = formatLocalTime(reminder.remindAt, timezone); const eventAt = item.startAt ?? item.dueAt ?? null; const eventLabel = eventAt ? formatLocalTime(eventAt, timezone) : null; const lines = [`\u3010\u5F85\u529E\u63D0\u9192\u3011${item.title}`]; if (eventLabel && eventLabel !== remindLabel) { lines.push(`\u4E8B\u9879\u65F6\u95F4\uFF1A${eventLabel}`); } lines.push(`\u63D0\u9192\u65F6\u95F4\uFF1A${remindLabel}`); if (item.location) lines.push(`\u5730\u70B9\uFF1A${item.location}`); return lines.join("\n"); }; const listDueBalanceAlerts = async ({ now = clock.now(), limit = 50 } = {}) => { const [rows] = await pool.query( `SELECT * FROM h5_balance_alert_subscriptions WHERE next_run_at <= ? AND (status = 'active' OR (status = 'locked' AND locked_until IS NOT NULL AND locked_until <= ?)) ORDER BY next_run_at ASC LIMIT ?`, [now, now, Math.max(1, Math.min(200, Number(limit) || 50))] ); return rows.map(rowToBalanceAlert); }; const lockBalanceAlert = async (id, { now = clock.now(), lockMs = 12e4 } = {}) => { const lockedUntil = now + lockMs; const [result] = await pool.query( `UPDATE h5_balance_alert_subscriptions SET status = 'locked', locked_until = ?, attempts = attempts + 1, updated_at = ? WHERE id = ? AND (status = 'active' OR (status = 'locked' AND locked_until IS NOT NULL AND locked_until <= ?))`, [lockedUntil, now, id, now] ); if (Number(result?.affectedRows ?? 0) !== 1) return null; const [rows] = await pool.query( `SELECT * FROM h5_balance_alert_subscriptions WHERE id = ? LIMIT 1`, [id] ); return rowToBalanceAlert(rows[0]); }; const markBalanceAlertSent = async (subscription, { now = clock.now() } = {}) => { await pool.query( `UPDATE h5_balance_alert_subscriptions SET status = 'active', next_run_at = ?, last_run_at = ?, last_notified_balance_cents = ?, locked_until = NULL, last_error = NULL, updated_at = ? WHERE id = ?`, [now + 5 * 60 * 1e3, now, subscription.lastNotifiedBalanceCents ?? null, now, subscription.id] ); return { ...subscription, status: "active", lastRunAt: now }; }; const markBalanceAlertFailed = async (subscription, error, { now = clock.now(), retryMs = 10 * 60 * 1e3, maxAttempts = 5 } = {}) => { const attempts = Number(subscription.attempts ?? 0); const status = attempts >= maxAttempts ? "failed" : "active"; const nextRunAt = status === "active" ? now + retryMs : subscription.nextRunAt; await pool.query( `UPDATE h5_balance_alert_subscriptions SET status = ?, next_run_at = ?, locked_until = NULL, last_error = ?, updated_at = ? WHERE id = ?`, [status, nextRunAt, String(error?.message ?? error ?? "\u53D1\u9001\u5931\u8D25").slice(0, 500), now, subscription.id] ); }; const listDueDigestSubscriptions = async ({ now = clock.now(), limit = 50 } = {}) => { const [rows] = await pool.query( `SELECT * FROM h5_schedule_digest_subscriptions WHERE next_run_at <= ? AND (status = 'active' OR (status = 'locked' AND locked_until IS NOT NULL AND locked_until <= ?)) ORDER BY next_run_at ASC LIMIT ?`, [now, now, Math.max(1, Math.min(200, Number(limit) || 50))] ); return rows.map(rowToDigest); }; const lockDigestSubscription = async (id, { now = clock.now(), lockMs = 12e4 } = {}) => { const lockedUntil = now + lockMs; const [result] = await pool.query( `UPDATE h5_schedule_digest_subscriptions SET status = 'locked', locked_until = ?, attempts = attempts + 1, updated_at = ? WHERE id = ? AND (status = 'active' OR (status = 'locked' AND locked_until IS NOT NULL AND locked_until <= ?))`, [lockedUntil, now, id, now] ); if (Number(result?.affectedRows ?? 0) !== 1) return null; const [rows] = await pool.query( `SELECT * FROM h5_schedule_digest_subscriptions WHERE id = ? LIMIT 1`, [id] ); return rowToDigest(rows[0]); }; const markDigestSent = async (subscription, { now = clock.now() } = {}) => { const nextRunAt = nextDailyRunAt({ hour: subscription.hour, minute: subscription.minute, timezone: subscription.timezone, now: now + 1e3 }); await pool.query( `UPDATE h5_schedule_digest_subscriptions SET status = 'active', next_run_at = ?, last_run_at = ?, locked_until = NULL, last_error = NULL, updated_at = ? WHERE id = ?`, [nextRunAt, now, now, subscription.id] ); return { ...subscription, status: "active", nextRunAt, lastRunAt: now }; }; const markDigestFailed = async (subscription, error, { now = clock.now(), retryMs = 10 * 60 * 1e3, maxAttempts = 5 } = {}) => { const attempts = Number(subscription.attempts ?? 0); const status = attempts >= maxAttempts ? "failed" : "active"; const nextRunAt = status === "active" ? now + retryMs : subscription.nextRunAt; await pool.query( `UPDATE h5_schedule_digest_subscriptions SET status = ?, next_run_at = ?, locked_until = NULL, last_error = ?, updated_at = ? WHERE id = ?`, [status, nextRunAt, String(error?.message ?? error ?? "\u53D1\u9001\u5931\u8D25").slice(0, 500), now, subscription.id] ); }; const logDelivery = async ({ reminderId = null, subscriptionId = null, userId, channel = "wechat", status, providerMessageId = null, errorCode = null, errorMessage = null }) => { await pool.query( `INSERT INTO h5_schedule_delivery_logs (id, reminder_id, subscription_id, user_id, channel, status, provider_message_id, error_code, error_message, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ crypto.randomUUID(), reminderId, subscriptionId, userId, channel, status, providerMessageId, errorCode, errorMessage ? String(errorMessage).slice(0, 500) : null, clock.now() ] ); }; const buildTodoDigestText = async ({ userId, timezone = defaultTimezone, now = clock.now() }) => { const items = await listTodayTodoItems({ userId, timezone, now }); return formatTodoDigest(items, { now, timezone }); }; const getUserWalletSnapshot = async (userId) => { if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237"); const [rows] = await pool.query( `SELECT balance_cents AS balanceCents, tokens_used AS tokensUsed FROM h5_user_wallets WHERE user_id = ? LIMIT 1`, [userId] ); return rows[0] ?? null; }; const createUserNotification = async ({ userId, channel = "web", notificationType, title, body, data = null }) => { if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237"); if (!notificationType) throw new Error("\u7F3A\u5C11\u901A\u77E5\u7C7B\u578B"); if (!title) throw new Error("\u7F3A\u5C11\u901A\u77E5\u6807\u9898"); if (!body) throw new Error("\u7F3A\u5C11\u901A\u77E5\u5185\u5BB9"); const now = clock.now(); const id = crypto.randomUUID(); await pool.query( `INSERT INTO h5_user_notifications (id, user_id, channel, notification_type, title, body, data_json, status, read_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'unread', NULL, ?, ?)`, [id, userId, channel, notificationType, title, body, data ? JSON.stringify(data) : null, now, now] ); return { id, userId, channel, notificationType, title, body, data, status: "unread", readAt: null, createdAt: now, updatedAt: now }; }; const listUserNotifications = async ({ userId, status = "unread", limit = 20 } = {}) => { if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237"); const clauses = ["user_id = ?"]; const params = [userId]; if (status && status !== "all") { clauses.push("status = ?"); params.push(status); } params.push(Math.max(1, Math.min(100, Number(limit) || 20))); const [rows] = await pool.query( `SELECT * FROM h5_user_notifications WHERE ${clauses.join(" AND ")} ORDER BY created_at DESC LIMIT ?`, params ); return rows.map(rowToUserNotification); }; const markUserNotificationRead = async ({ userId, notificationId }) => { if (!userId || !notificationId) throw new Error("\u7F3A\u5C11\u901A\u77E5\u53C2\u6570"); const now = clock.now(); const [result] = await pool.query( `UPDATE h5_user_notifications SET status = 'read', read_at = ?, updated_at = ? WHERE id = ? AND user_id = ? AND status <> 'read'`, [now, now, notificationId, userId] ); return Number(result?.affectedRows ?? 0) > 0; }; const markAllUserNotificationsRead = async ({ userId } = {}) => { if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237"); const now = clock.now(); const [result] = await pool.query( `UPDATE h5_user_notifications SET status = 'read', read_at = ?, updated_at = ? WHERE user_id = ? AND status <> 'read'`, [now, now, userId] ); return Number(result?.affectedRows ?? 0); }; const deleteUserNotification = async ({ userId, notificationId } = {}) => { if (!userId || !notificationId) throw new Error("\u7F3A\u5C11\u901A\u77E5\u53C2\u6570"); const [result] = await pool.query( `DELETE FROM h5_user_notifications WHERE id = ? AND user_id = ?`, [notificationId, userId] ); return Number(result?.affectedRows ?? 0) > 0; }; const clearUserNotifications = async ({ userId, status = "all" } = {}) => { if (!userId) throw new Error("\u7F3A\u5C11\u7528\u6237"); const clauses = ["user_id = ?"]; const params = [userId]; if (status && status !== "all") { clauses.push("status = ?"); params.push(status); } const [result] = await pool.query( `DELETE FROM h5_user_notifications WHERE ${clauses.join(" AND ")}`, params ); return Number(result?.affectedRows ?? 0); }; return { createItem, getItem, createReminder, listItems, listItemsBySourceMessage, listTodayTodoItems, listUpcomingItems, listUpcomingReminders, getReminder, cancelReminder, deleteReminder, deleteReminders, listDigestSubscriptions, createDailyTodoDigest, createBalanceLowAlert, listDueReminders, lockReminder, markReminderSent, markReminderCancelled, markReminderFailed, buildReminderText, listDueDigestSubscriptions, lockDigestSubscription, markDigestSent, markDigestFailed, listDueBalanceAlerts, lockBalanceAlert, markBalanceAlertSent, markBalanceAlertFailed, logDelivery, buildTodoDigestText, getUserWalletSnapshot, createUserNotification, listUserNotifications, markUserNotificationRead, markAllUserNotificationsRead, deleteUserNotification, clearUserNotifications }; } // mindspace-long-image.mjs import fs from "node:fs"; import fsPromises from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; var DEFAULT_VIEWPORT_WIDTH = 1280; var DEFAULT_VIEWPORT_HEIGHT = 720; var MAX_LONG_IMAGE_HEIGHT = 2e4; function longImagePathForHtml(htmlPath, outputPath = null) { return outputPath || String(htmlPath).replace(/\.html$/i, ".long.png"); } function clampDimension(value, fallback, max) { const number = Math.ceil(Number(value) || fallback); return Math.max(320, Math.min(number, max)); } async function launchChromium(chromium) { const base = { headless: true, args: ["--disable-dev-shm-usage", "--hide-scrollbars"] }; if (process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) { return chromium.launch({ ...base, executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH }); } if (process.env.PLAYWRIGHT_CHROMIUM_CHANNEL) { return chromium.launch({ ...base, channel: process.env.PLAYWRIGHT_CHROMIUM_CHANNEL }); } try { return await chromium.launch(base); } catch (error) { if (process.platform === "darwin") { return chromium.launch({ ...base, channel: "chrome" }); } throw error; } } async function renderLongImage({ htmlPath = null, url = null, outputPath = null, viewportWidth = DEFAULT_VIEWPORT_WIDTH, viewportHeight = DEFAULT_VIEWPORT_HEIGHT } = {}) { if (!htmlPath && !url) throw new Error("\u7F3A\u5C11 htmlPath \u6216 url"); const targetUrl = url || pathToFileURL(path.resolve(htmlPath)).toString(); const destination = outputPath ? path.resolve(outputPath) : longImagePathForHtml(path.resolve(htmlPath)); const { chromium } = await import("playwright"); let browser = null; try { browser = await launchChromium(chromium); const page = await browser.newPage({ viewport: { width: clampDimension(viewportWidth, DEFAULT_VIEWPORT_WIDTH, 2400), height: clampDimension(viewportHeight, DEFAULT_VIEWPORT_HEIGHT, MAX_LONG_IMAGE_HEIGHT) }, deviceScaleFactor: 2 }); await page.goto(targetUrl, { waitUntil: "networkidle", timeout: 3e4 }); await page.evaluate(() => document.fonts?.ready).catch(() => null); const size = await page.evaluate(() => ({ width: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth, 320), height: Math.max(document.documentElement.scrollHeight, document.body.scrollHeight, 320) })); await page.setViewportSize({ width: clampDimension(size.width, DEFAULT_VIEWPORT_WIDTH, 2400), height: Math.min(clampDimension(size.height, DEFAULT_VIEWPORT_HEIGHT, MAX_LONG_IMAGE_HEIGHT), 2400) }); await fsPromises.mkdir(path.dirname(destination), { recursive: true }); await page.screenshot({ path: destination, fullPage: true, type: "png", animations: "disabled", caret: "hide" }); return { outputPath: destination, bytes: fs.statSync(destination).size }; } finally { await browser?.close().catch(() => { }); } } // mindspace-sandbox-mcp.mjs var SANDBOX_ROOT = process.argv[2]?.trim() || process.env.SANDBOX_ROOT?.trim(); if (!SANDBOX_ROOT) { process.stderr.write("[mindspace-sandbox-mcp] SANDBOX_ROOT is not set \u2014 refusing to start\n"); process.exit(1); } var SANDBOX = path2.resolve(SANDBOX_ROOT); var PRIVATE_DATA_DIR = path2.join(SANDBOX, ".mindspace"); var PRIVATE_DATA_DB = path2.join(PRIVATE_DATA_DIR, "private-data.sqlite"); var SQLITE_BIN = process.env.SQLITE_BIN?.trim() || "sqlite3"; var PRIVATE_DATA_MAX_BYTES = Number(process.env.PRIVATE_DATA_MAX_BYTES ?? 20 * 1024 * 1024); var PRIVATE_DATA_QUERY_TIMEOUT_MS = Number(process.env.PRIVATE_DATA_QUERY_TIMEOUT_MS ?? 5e3); var PRIVATE_DATA_MAX_ROWS = Number(process.env.PRIVATE_DATA_MAX_ROWS ?? 200); var PRIVATE_DATA_USER_ID = process.env.PRIVATE_DATA_USER_ID?.trim(); var allowedToolsEnv = process.env.ALLOWED_TOOLS?.trim(); var ALLOWED_TOOLS = allowedToolsEnv ? new Set(allowedToolsEnv.split(",").map((s) => s.trim())) : null; function resolveSandboxed(p) { if (!p || typeof p !== "string") throw new Error("\u8DEF\u5F84\u53C2\u6570\u65E0\u6548"); const resolved = path2.isAbsolute(p) ? path2.resolve(p) : path2.resolve(SANDBOX, p); if (resolved !== SANDBOX && !resolved.startsWith(SANDBOX + path2.sep)) { throw Object.assign(new Error(`\u8DEF\u5F84\u8D8A\u754C\uFF1A${p} \u4E0D\u5728\u5F53\u524D\u5DE5\u4F5C\u533A\u5185`), { code: "EACCES" }); } if (resolved === PRIVATE_DATA_DIR || resolved.startsWith(PRIVATE_DATA_DIR + path2.sep)) { throw Object.assign(new Error("\u7981\u6B62\u76F4\u63A5\u8BBF\u95EE\u79C1\u6709\u6570\u636E\u76EE\u5F55\uFF0C\u8BF7\u4F7F\u7528 private_data_* \u5DE5\u5177"), { code: "EACCES" }); } return resolved; } var ALL_TOOLS = [ { name: "read_file", description: "\u8BFB\u53D6\u6587\u4EF6\u5185\u5BB9\uFF08\u4EC5\u9650\u5DE5\u4F5C\u533A\u5185\u7684\u6587\u4EF6\uFF09", inputSchema: { type: "object", properties: { path: { type: "string", description: "\u6587\u4EF6\u8DEF\u5F84\uFF08\u76F8\u5BF9\u5DE5\u4F5C\u533A\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF09" } }, required: ["path"] } }, { name: "write_file", description: "\u5199\u5165\u6587\u4EF6\u5185\u5BB9\uFF08\u4EC5\u9650\u5DE5\u4F5C\u533A\u5185\u7684\u6587\u4EF6\uFF1B\u4E0D\u5B58\u5728\u5219\u521B\u5EFA\uFF0C\u5DF2\u5B58\u5728\u5219\u8986\u76D6\uFF09", inputSchema: { type: "object", properties: { path: { type: "string", description: "\u6587\u4EF6\u8DEF\u5F84" }, content: { type: "string", description: "\u6587\u4EF6\u5185\u5BB9" } }, required: ["path", "content"] } }, { name: "edit_file", description: "\u5C06\u6587\u4EF6\u4E2D\u7684\u65E7\u5185\u5BB9\u66FF\u6362\u4E3A\u65B0\u5185\u5BB9\uFF08\u4EC5\u9650\u5DE5\u4F5C\u533A\u5185\u7684\u6587\u4EF6\uFF09", inputSchema: { type: "object", properties: { path: { type: "string", description: "\u6587\u4EF6\u8DEF\u5F84" }, old_str: { type: "string", description: "\u8981\u66FF\u6362\u7684\u539F\u59CB\u5185\u5BB9\uFF08\u5FC5\u987B\u5728\u6587\u4EF6\u4E2D\u552F\u4E00\uFF09" }, new_str: { type: "string", description: "\u66FF\u6362\u540E\u7684\u65B0\u5185\u5BB9" } }, required: ["path", "old_str", "new_str"] } }, { name: "list_dir", description: "\u5217\u51FA\u76EE\u5F55\u5185\u5BB9\uFF08\u4EC5\u9650\u5DE5\u4F5C\u533A\u5185\u7684\u76EE\u5F55\uFF09", inputSchema: { type: "object", properties: { path: { type: "string", description: "\u76EE\u5F55\u8DEF\u5F84\uFF08\u7701\u7565\u5219\u5217\u51FA\u5DE5\u4F5C\u533A\u6839\u76EE\u5F55\uFF09" } } } }, { name: "create_dir", description: "\u521B\u5EFA\u76EE\u5F55\uFF08\u4EC5\u9650\u5DE5\u4F5C\u533A\u5185\uFF1B\u5DF2\u5B58\u5728\u4E0D\u62A5\u9519\uFF09", inputSchema: { type: "object", properties: { path: { type: "string", description: "\u76EE\u5F55\u8DEF\u5F84" } }, required: ["path"] } }, { name: "generate_long_image", description: "\u7528 Playwright \u5C06\u5DE5\u4F5C\u533A\u5185\u7684 HTML \u9875\u9762\u6E32\u67D3\u4E3A\u6574\u9875 PNG \u957F\u56FE\u3002\u8F93\u51FA\u6587\u4EF6\u901A\u5E38\u4E3A public/<\u9875\u9762\u540D>.long.png\uFF0C\u5FC5\u987B\u5728\u5DE5\u4F5C\u533A\u5185\u3002", inputSchema: { type: "object", properties: { html_path: { type: "string", description: "HTML \u6587\u4EF6\u8DEF\u5F84\uFF0C\u5982 public/report.html" }, output_path: { type: "string", description: "\u8F93\u51FA PNG \u8DEF\u5F84\uFF0C\u5982 public/report.long.png\uFF1B\u53EF\u9009" } }, required: ["html_path"] } }, { name: "private_data_info", description: "\u67E5\u770B\u5F53\u524D\u7528\u6237\u201C\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u201D\u7684\u72B6\u6001\u3002\u6BCF\u4E2A\u7528\u6237\u53EA\u6709\u4E00\u4E2A\u79C1\u6709 SQLite \u6570\u636E\u5E93\uFF0C\u9002\u5408\u4FDD\u5B58\u95EE\u5377\u3001\u8868\u5355\u3001\u6E05\u5355\u3001\u8C03\u7814\u6570\u636E\u548C\u5206\u6790\u4E2D\u95F4\u8868\uFF1B\u4E0D\u8981\u521B\u5EFA\u989D\u5916 SQLite \u6587\u4EF6\u3002", inputSchema: { type: "object", properties: {} } }, { name: "private_data_schema", description: "\u67E5\u770B\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u4E2D\u7684\u8868\u548C\u5B57\u6BB5\u3002\u7528\u4E8E\u4E86\u89E3\u5F53\u524D\u7528\u6237\u81EA\u5DF1\u7684 SQLite \u8868\u7ED3\u6784\u3002", inputSchema: { type: "object", properties: {} } }, { name: "private_data_query", description: "\u53EA\u8BFB\u67E5\u8BE2\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u3002\u4EC5\u5141\u8BB8 SELECT/WITH\uFF0C\u9ED8\u8BA4\u6700\u591A\u8FD4\u56DE 200 \u884C\uFF0C\u9002\u5408\u7EDF\u8BA1\u95EE\u5377\u56DE\u7B54\u3001\u7B5B\u9009\u8868\u5355\u6570\u636E\u3001\u5206\u6790\u7528\u6237\u79C1\u6709\u6570\u636E\u3002", inputSchema: { type: "object", properties: { sql: { type: "string", description: "\u53EA\u8BFB SQL\uFF0C\u5FC5\u987B\u662F SELECT/WITH" } }, required: ["sql"] } }, { name: "private_data_execute", description: "\u5199\u5165\u6216\u53D8\u66F4\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u3002\u53EF\u7528\u4E8E\u521B\u5EFA\u95EE\u5377/\u8868\u5355/\u6E05\u5355\u7B49\u79C1\u6709\u8868\u5E76\u5199\u5165\u6570\u636E\uFF1B\u7981\u6B62 ATTACH\u3001load_extension\u3001PRAGMA writable_schema\u3001VACUUM INTO \u7B49\u8D8A\u754C\u6216\u5371\u9669\u64CD\u4F5C\u3002", inputSchema: { type: "object", properties: { sql: { type: "string", description: "\u8981\u6267\u884C\u7684 SQLite SQL" } }, required: ["sql"] } } ]; var quotaPool = null; var scheduleService = null; function isQuotaSyncConfigured() { return Boolean( PRIVATE_DATA_USER_ID && (process.env.DATABASE_URL || process.env.MYSQL_HOST && process.env.MYSQL_DATABASE) ); } function getQuotaPool() { if (!isQuotaSyncConfigured()) return null; if (quotaPool) return quotaPool; quotaPool = process.env.DATABASE_URL ? mysql.createPool(process.env.DATABASE_URL) : mysql.createPool({ host: process.env.MYSQL_HOST ?? "localhost", port: Number(process.env.MYSQL_PORT ?? 3306), user: process.env.MYSQL_USER ?? "boot", password: process.env.MYSQL_PASSWORD ?? "", database: process.env.MYSQL_DATABASE ?? "tkmind", waitForConnections: true, connectionLimit: 2 }); return quotaPool; } function isScheduleConfigured() { return isQuotaSyncConfigured(); } function getScheduleService() { if (!PRIVATE_DATA_USER_ID) throw new Error("\u5F53\u524D\u4F1A\u8BDD\u6CA1\u6709\u7528\u6237\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u80FD\u64CD\u4F5C\u5F85\u529E"); const pool = getQuotaPool(); if (!pool || !isScheduleConfigured()) { throw new Error("\u5F53\u524D\u73AF\u5883\u672A\u914D\u7F6E\u5F85\u529E\u6570\u636E\u5E93\u8FDE\u63A5"); } if (!scheduleService) { scheduleService = createScheduleService(pool, { defaultTimezone: process.env.H5_DEFAULT_TIMEZONE || "Asia/Shanghai" }); } return scheduleService; } if (isScheduleConfigured()) { ALL_TOOLS.push( { name: "schedule_create_item", description: "\u4E3A\u5F53\u524D\u7528\u6237\u521B\u5EFA\u5F85\u529E\u6216\u65E5\u7A0B\u4E8B\u9879\u3002\u4EC5\u5199\u5165\u5F53\u524D\u7528\u6237\u81EA\u5DF1\u7684 h5_schedule_items\uFF0C\u4E0D\u53EF\u64CD\u4F5C\u5176\u4ED6\u7528\u6237\u6570\u636E\u3002", inputSchema: { type: "object", properties: { kind: { type: "string", description: "task \u6216 event\uFF0C\u9ED8\u8BA4 task" }, title: { type: "string", description: "\u4E8B\u9879\u6807\u9898" }, description: { type: "string", description: "\u4E8B\u9879\u63CF\u8FF0\uFF0C\u53EF\u9009" }, startAt: { type: "number", description: "\u5F00\u59CB\u65F6\u95F4 Unix \u6BEB\u79D2\u65F6\u95F4\u6233\uFF0C\u53EF\u9009\uFF08\u4F18\u5148\u4F7F\u7528 startLocal\uFF09" }, startLocal: { type: "string", description: "\u672C\u5730\u5F00\u59CB\u65F6\u95F4 YYYY-MM-DD HH:mm\uFF0C\u63A8\u8350" }, endAt: { type: "number", description: "\u7ED3\u675F\u65F6\u95F4 Unix \u6BEB\u79D2\u65F6\u95F4\u6233\uFF0C\u53EF\u9009\uFF08\u4F18\u5148\u4F7F\u7528 endLocal\uFF09" }, endLocal: { type: "string", description: "\u672C\u5730\u7ED3\u675F\u65F6\u95F4 YYYY-MM-DD HH:mm\uFF0C\u63A8\u8350" }, dueAt: { type: "number", description: "\u622A\u6B62\u65F6\u95F4 Unix \u6BEB\u79D2\u65F6\u95F4\u6233\uFF0C\u53EF\u9009\uFF08\u4F18\u5148\u4F7F\u7528 dueLocal\uFF09" }, dueLocal: { type: "string", description: "\u672C\u5730\u622A\u6B62\u65F6\u95F4 YYYY-MM-DD HH:mm\uFF0C\u53EF\u9009" }, allDay: { type: "boolean", description: "\u662F\u5426\u5168\u5929\u4E8B\u9879\uFF0C\u53EF\u9009" }, timezone: { type: "string", description: "\u65F6\u533A\uFF0C\u53EF\u9009" }, location: { type: "string", description: "\u5730\u70B9\uFF0C\u53EF\u9009" }, sourceMessageId: { type: "string", description: "\u670D\u52A1\u53F7\u6D88\u606F ID\uFF1B\u6709\u503C\u65F6\u5FC5\u987B\u539F\u6837\u4F20\u5165" }, sourceText: { type: "string", description: "\u539F\u59CB\u7528\u6237\u6587\u672C\uFF0C\u53EF\u9009" } }, required: ["title"] } }, { name: "schedule_create_reminder", description: "\u4E3A\u5F53\u524D\u7528\u6237\u5DF2\u6709\u4E8B\u9879\u521B\u5EFA\u5355\u6B21\u63D0\u9192\u3002\u5FC5\u987B\u5148\u6709 itemId\uFF0C\u518D\u5199\u5165 h5_schedule_reminders\u3002", inputSchema: { type: "object", properties: { itemId: { type: "string", description: "\u4E8B\u9879 ID" }, remindAt: { type: "number", description: "\u63D0\u9192\u89E6\u53D1\u65F6\u95F4 Unix \u6BEB\u79D2\u65F6\u95F4\u6233\uFF08\u4F18\u5148\u4F7F\u7528 remindLocal\uFF09" }, remindLocal: { type: "string", description: "\u672C\u5730\u63D0\u9192\u65F6\u95F4 YYYY-MM-DD HH:mm\uFF0C\u63A8\u8350" }, offsetMinutes: { type: "number", description: "\u76F8\u5BF9\u4E8B\u9879\u65F6\u95F4\u7684\u63D0\u524D\u5206\u949F\u6570\uFF0C\u53EF\u9009" }, channel: { type: "string", description: "\u63D0\u9192\u901A\u9053\uFF0C\u9ED8\u8BA4 wechat" } }, required: ["itemId"] } }, { name: "schedule_list_items", description: "\u67E5\u8BE2\u5F53\u524D\u7528\u6237\u7684\u5F85\u529E/\u65E5\u7A0B\u4E8B\u9879\u5217\u8868\u3002\u4EC5\u8FD4\u56DE\u5F53\u524D\u7528\u6237\u6570\u636E\uFF0C\u53EF\u6309\u65F6\u95F4\u8303\u56F4\u8FC7\u6EE4\u3002", inputSchema: { type: "object", properties: { from: { type: "number", description: "\u8D77\u59CB\u65F6\u95F4 Unix \u6BEB\u79D2\u65F6\u95F4\u6233\uFF0C\u53EF\u9009" }, to: { type: "number", description: "\u7ED3\u675F\u65F6\u95F4 Unix \u6BEB\u79D2\u65F6\u95F4\u6233\uFF0C\u53EF\u9009" }, status: { type: "string", description: "\u4E8B\u9879\u72B6\u6001\uFF0C\u9ED8\u8BA4 active\uFF0C\u53EF\u9009" }, limit: { type: "number", description: "\u8FD4\u56DE\u6570\u91CF\uFF0C\u9ED8\u8BA4 50\uFF0C\u53EF\u9009" } } } } ); } var TOOLS = ALLOWED_TOOLS ? ALL_TOOLS.filter((t) => ALLOWED_TOOLS.has(t.name)) : ALL_TOOLS; function ensurePrivateDataDb() { fs2.mkdirSync(PRIVATE_DATA_DIR, { recursive: true }); if (!fs2.existsSync(PRIVATE_DATA_DB)) { runSqlite(["-batch", PRIVATE_DATA_DB, "PRAGMA journal_mode=WAL; PRAGMA user_version = 1;"]); } return PRIVATE_DATA_DB; } function privateDataSize() { let total = 0; for (const file of fs2.existsSync(PRIVATE_DATA_DIR) ? fs2.readdirSync(PRIVATE_DATA_DIR) : []) { if (file === "private-data.sqlite" || file.startsWith("private-data.sqlite-")) { total += fs2.statSync(path2.join(PRIVATE_DATA_DIR, file)).size; } } return total; } function runSqlite(args) { return execFileSync(SQLITE_BIN, args, { encoding: "utf8", timeout: PRIVATE_DATA_QUERY_TIMEOUT_MS, maxBuffer: 1024 * 1024 }); } function sqliteScalar(sql) { return Number(runSqlite(["-batch", "-noheader", PRIVATE_DATA_DB, sql]).trim() || 0); } async function getQuotaState({ forUpdate = false, conn = null } = {}) { const pool = getQuotaPool(); if (!pool) return null; const db = conn ?? pool; const [rows] = await db.query( `SELECT id, quota_bytes, used_bytes, reserved_bytes, status FROM h5_user_spaces WHERE user_id = ? LIMIT 1 ${forUpdate ? "FOR UPDATE" : ""}`, [PRIVATE_DATA_USER_ID] ); const row = rows[0]; if (!row) return null; const quotaBytes = Number(row.quota_bytes ?? 0); const usedBytes = Number(row.used_bytes ?? 0); const reservedBytes = Number(row.reserved_bytes ?? 0); return { id: row.id, status: row.status, quotaBytes, usedBytes, reservedBytes, availableBytes: Math.max(0, quotaBytes - usedBytes - reservedBytes) }; } async function sqliteMaxPagePragmaForQuota(currentBytes) { const quota = await getQuotaState(); if (!quota) return ""; if (quota.status !== "active") throw new Error("\u7528\u6237\u7A7A\u95F4\u4E0D\u53EF\u5199"); if (quota.availableBytes <= 0 && currentBytes >= PRIVATE_DATA_MAX_BYTES) { throw new Error("\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u914D\u989D\u4E0D\u8DB3"); } const allowedSize = Math.min(PRIVATE_DATA_MAX_BYTES, currentBytes + quota.availableBytes); const pageSize = sqliteScalar("PRAGMA page_size;") || 4096; const currentPages = sqliteScalar("PRAGMA page_count;") || 1; const maxPages = Math.max(currentPages, Math.max(1, Math.floor(allowedSize / pageSize))); return `PRAGMA max_page_count=${maxPages};`; } async function syncPrivateDataQuota(deltaBytes) { const pool = getQuotaPool(); if (!pool || !deltaBytes) return null; const conn = await pool.getConnection(); try { await conn.beginTransaction(); const quota = await getQuotaState({ forUpdate: true, conn }); if (!quota) { await conn.rollback(); return null; } if (quota.status !== "active") throw new Error("\u7528\u6237\u7A7A\u95F4\u4E0D\u53EF\u5199"); if (deltaBytes > quota.availableBytes) { throw new Error(`\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u914D\u989D\u4E0D\u8DB3\uFF1A\u9700\u8981 ${deltaBytes} \u5B57\u8282\uFF0C\u53EF\u7528 ${quota.availableBytes} \u5B57\u8282`); } await conn.query( `UPDATE h5_user_spaces SET used_bytes = GREATEST(0, used_bytes + ?), updated_at = ? WHERE id = ? AND user_id = ?`, [deltaBytes, Date.now(), quota.id, PRIVATE_DATA_USER_ID] ); await conn.commit(); return { deltaBytes }; } catch (err) { await conn.rollback(); throw err; } finally { conn.release(); } } function stripSqlComments(sql) { return String(sql ?? "").replace(/--.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "").trim(); } function rejectDangerousSql(sql, { readonly = false } = {}) { const cleaned = stripSqlComments(sql); if (!cleaned) throw new Error("SQL \u4E0D\u80FD\u4E3A\u7A7A"); if (cleaned.length > 2e4) throw new Error("SQL \u8FC7\u957F"); if (/^\s*\./m.test(cleaned)) { throw new Error("SQL \u4E0D\u5141\u8BB8\u4F7F\u7528 sqlite3 dot command"); } if (/\b(ATTACH|DETACH|LOAD_EXTENSION|VACUUM\s+INTO)\b/i.test(cleaned)) { throw new Error("SQL \u5305\u542B\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u4E0D\u5141\u8BB8\u7684\u64CD\u4F5C"); } if (/\bPRAGMA\s+writable_schema\b/i.test(cleaned)) { throw new Error("SQL \u5305\u542B\u4E0D\u5141\u8BB8\u7684 PRAGMA"); } if (readonly && !/^\s*(SELECT|WITH)\b/i.test(cleaned)) { throw new Error("private_data_query \u53EA\u5141\u8BB8 SELECT/WITH"); } return cleaned; } async function queryPrivateData(sql) { const db = ensurePrivateDataDb(); const cleaned = rejectDangerousSql(sql, { readonly: true }); const limited = `SELECT * FROM (${cleaned.replace(/;\s*$/, "")}) LIMIT ${PRIVATE_DATA_MAX_ROWS}`; const output = runSqlite(["-json", db, limited]); return output.trim() || "[]"; } async function executePrivateData(sql) { const db = ensurePrivateDataDb(); const before = privateDataSize(); if (before > PRIVATE_DATA_MAX_BYTES) throw new Error("\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u5DF2\u8D85\u8FC7\u5927\u5C0F\u9650\u5236"); const cleaned = rejectDangerousSql(sql); const quotaPragma = await sqliteMaxPagePragmaForQuota(before); runSqlite(["-batch", db, `${quotaPragma} ${cleaned}`]); const after = privateDataSize(); if (after > PRIVATE_DATA_MAX_BYTES) { throw new Error(`\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4\u8D85\u8FC7\u5927\u5C0F\u9650\u5236\uFF1A${after}/${PRIVATE_DATA_MAX_BYTES} \u5B57\u8282`); } const delta = after - before; const quotaSync = await syncPrivateDataQuota(delta); return `\u5DF2\u6267\u884C\u3002\u5F53\u524D\u6570\u636E\u7A7A\u95F4\u5927\u5C0F ${after} \u5B57\u8282${quotaSync ? `\uFF0C\u5DF2\u540C\u6B65\u7A7A\u95F4\u5360\u7528 ${delta} \u5B57\u8282` : ""}`; } async function privateDataSchema() { const db = ensurePrivateDataDb(); const output = runSqlite([ "-json", db, `SELECT m.name AS table_name, p.cid, p.name AS column_name, p.type, p."notnull" AS not_null, p.pk FROM sqlite_master m JOIN pragma_table_info(m.name) p WHERE m.type = 'table' AND m.name NOT LIKE 'sqlite_%' ORDER BY m.name, p.cid` ]); return output.trim() || "[]"; } async function callTool(name, args) { if (ALLOWED_TOOLS && !ALLOWED_TOOLS.has(name)) { throw new Error(`\u5DE5\u5177 ${name} \u672A\u6388\u6743`); } switch (name) { case "read_file": { const abs = resolveSandboxed(args.path); const content = fs2.readFileSync(abs, "utf8"); return [{ type: "text", text: content }]; } case "write_file": { const abs = resolveSandboxed(args.path); fs2.mkdirSync(path2.dirname(abs), { recursive: true }); fs2.writeFileSync(abs, args.content ?? "", "utf8"); return [{ type: "text", text: `\u5DF2\u5199\u5165 ${args.path}\uFF08${(args.content ?? "").length} \u5B57\u8282\uFF09` }]; } case "edit_file": { const abs = resolveSandboxed(args.path); const original = fs2.readFileSync(abs, "utf8"); const oldStr = args.old_str ?? ""; if (oldStr && !original.includes(oldStr)) { throw new Error("edit_file: old_str \u5728\u6587\u4EF6\u4E2D\u4E0D\u5B58\u5728\uFF0C\u66FF\u6362\u5931\u8D25"); } const updated = oldStr ? original.replace(oldStr, args.new_str ?? "") : args.new_str ?? ""; fs2.writeFileSync(abs, updated, "utf8"); return [{ type: "text", text: `\u5DF2\u7F16\u8F91 ${args.path}` }]; } case "list_dir": { const target = args?.path ?? "."; const abs = resolveSandboxed(target); const entries = fs2.readdirSync(abs, { withFileTypes: true }); const lines = entries.map((e) => `${e.isDirectory() ? "[\u76EE\u5F55]" : "[\u6587\u4EF6]"} ${e.name}`); return [{ type: "text", text: lines.join("\n") || "\uFF08\u7A7A\u76EE\u5F55\uFF09" }]; } case "create_dir": { const abs = resolveSandboxed(args.path); fs2.mkdirSync(abs, { recursive: true }); return [{ type: "text", text: `\u5DF2\u521B\u5EFA\u76EE\u5F55 ${args.path}` }]; } case "generate_long_image": { const htmlPath = String(args.html_path ?? args.path ?? "").trim(); if (!htmlPath.toLowerCase().endsWith(".html")) { throw new Error("generate_long_image: html_path \u5FC5\u987B\u662F .html \u6587\u4EF6"); } const htmlAbs = resolveSandboxed(htmlPath); const outputPath = String(args.output_path ?? "").trim() || htmlPath.replace(/\.html$/i, ".long.png"); if (!outputPath.toLowerCase().endsWith(".png")) { throw new Error("generate_long_image: output_path \u5FC5\u987B\u662F .png \u6587\u4EF6"); } const outputAbs = resolveSandboxed(outputPath); const result = await renderLongImage({ htmlPath: htmlAbs, outputPath: outputAbs }); return [ { type: "text", text: `\u5DF2\u751F\u6210\u957F\u56FE ${outputPath}\uFF08${result.bytes} \u5B57\u8282\uFF09` } ]; } case "private_data_info": { ensurePrivateDataDb(); const size = privateDataSize(); const quota = await getQuotaState().catch(() => null); return [ { type: "text", text: JSON.stringify( { name: "\u7528\u6237\u79C1\u6709\u6570\u636E\u7A7A\u95F4", database: ".mindspace/private-data.sqlite", maxBytes: PRIVATE_DATA_MAX_BYTES, sizeBytes: size, quotaSyncEnabled: Boolean(getQuotaPool()), quota: quota ? { status: quota.status, quotaBytes: quota.quotaBytes, usedBytes: quota.usedBytes, reservedBytes: quota.reservedBytes, availableBytes: quota.availableBytes } : null, rules: [ "\u6BCF\u4E2A\u7528\u6237\u53EA\u80FD\u4F7F\u7528\u8FD9\u4E2A\u552F\u4E00 SQLite \u6570\u636E\u5E93", "\u9002\u5408\u95EE\u5377\u3001\u8868\u5355\u3001\u6E05\u5355\u3001\u8C03\u7814\u6570\u636E\u548C\u5206\u6790\u4E2D\u95F4\u8868", "\u4E0D\u8981\u5B58\u8D26\u53F7\u3001\u8BA1\u8D39\u3001\u6743\u9650\u3001\u5BA1\u8BA1\u3001\u516C\u5F00\u5E73\u53F0\u6570\u636E\u6216\u8DE8\u7528\u6237\u6570\u636E" ] }, null, 2 ) } ]; } case "private_data_schema": return [{ type: "text", text: await privateDataSchema() }]; case "private_data_query": return [{ type: "text", text: await queryPrivateData(args.sql) }]; case "private_data_execute": return [{ type: "text", text: await executePrivateData(args.sql) }]; case "schedule_create_item": { const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? "Asia/Shanghai"; const startAt = resolveScheduleTimestamp({ epochMs: args.startAt, localString: args.startLocal, timezone, fieldName: "\u5F00\u59CB\u65F6\u95F4" }); const endAt = resolveScheduleTimestamp({ epochMs: args.endAt, localString: args.endLocal, timezone, fieldName: "\u7ED3\u675F\u65F6\u95F4" }); const dueAt = resolveScheduleTimestamp({ epochMs: args.dueAt, localString: args.dueLocal, timezone, fieldName: "\u622A\u6B62\u65F6\u95F4" }); const item = await getScheduleService().createItem({ userId: PRIVATE_DATA_USER_ID, kind: args.kind, title: args.title, description: args.description ?? null, startAt, endAt, dueAt, allDay: Boolean(args.allDay), timezone, location: args.location ?? null, sourceChannel: "agent", sourceMessageId: args.sourceMessageId ?? null, sourceText: args.sourceText ?? null, metadata: { source: "schedule_assistant_skill" } }); return [{ type: "text", text: JSON.stringify(item, null, 2) }]; } case "schedule_create_reminder": { const timezone = args.timezone ?? process.env.H5_DEFAULT_TIMEZONE ?? "Asia/Shanghai"; const remindAt = resolveScheduleTimestamp({ epochMs: args.remindAt, localString: args.remindLocal, timezone, fieldName: "\u63D0\u9192\u65F6\u95F4" }); if (remindAt == null) throw new Error("\u7F3A\u5C11\u63D0\u9192\u65F6\u95F4 remindLocal \u6216 remindAt"); const reminder = await getScheduleService().createReminder({ userId: PRIVATE_DATA_USER_ID, itemId: args.itemId, remindAt, offsetMinutes: args.offsetMinutes ?? null, channel: args.channel ?? "wechat" }); return [{ type: "text", text: JSON.stringify(reminder, null, 2) }]; } case "schedule_list_items": { const items = await getScheduleService().listItems({ userId: PRIVATE_DATA_USER_ID, from: args.from ?? null, to: args.to ?? null, status: args.status ?? "active", limit: args.limit ?? 50 }); return [{ type: "text", text: JSON.stringify(items, null, 2) }]; } default: throw new Error(`\u672A\u77E5\u5DE5\u5177\uFF1A${name}`); } } function send(obj) { process.stdout.write(JSON.stringify(obj) + "\n"); } function respond(id, result) { send({ jsonrpc: "2.0", id, result }); } function respondError(id, message, code = -32603) { send({ jsonrpc: "2.0", id, error: { code, message } }); } var rl = readline.createInterface({ input: process.stdin, terminal: false }); rl.on("line", async (raw) => { const line = raw.trim(); if (!line) return; let msg; try { msg = JSON.parse(line); } catch { process.stderr.write(`[mindspace-sandbox-mcp] invalid JSON: ${line} `); return; } const { id, method, params } = msg; switch (method) { case "initialize": respond(id, { protocolVersion: "2024-11-05", serverInfo: { name: "mindspace-sandbox", version: "1.0.0" }, capabilities: { tools: {} } }); break; case "initialized": break; case "tools/list": respond(id, { tools: TOOLS }); break; case "tools/call": { const { name, arguments: toolArgs } = params ?? {}; try { const content = await callTool(name, toolArgs ?? {}); respond(id, { content, isError: false }); } catch (err) { respond(id, { content: [{ type: "text", text: err.message }], isError: true }); } break; } default: if (id !== void 0 && id !== null) { respondError(id, `Method not found: ${method}`, -32601); } } }); rl.on("close", () => process.exit(0));