UNPKG

naisys

Version:

NAISYS - Autonomous AI agent runner with built-in context management and cost tracking

289 lines 11.5 kB
import { HubEvents } from "@naisys/hub-protocol"; import stringArgv from "string-argv"; import { mailCmd } from "../command/commandDefs.js"; import { formatMessageDisplay } from "./mailFormat.js"; export function createMailService(hubClient, userService, mailQueryService, localUserId, promptNotification, attachmentService, shellWrapper, globalConfig, agentManager, runService) { const localUser = userService.getUserById(localUserId); const localUsername = localUser?.username || "unknown"; const localTitle = localUser?.config.title || ""; async function handleCommand(args) { const argv = stringArgv(args); const subs = mailCmd.subcommands; const usageError = (sub) => `Invalid parameters. Usage: ${mailCmd.name} ${subs[sub].usage}`; if (!argv[0]) { argv[0] = "help"; } switch (argv[0]) { case "help": { const lines = [`${mailCmd.name} <command>`]; lines.push(` ${subs.send.usage.padEnd(40)}${subs.send.description}`); if (hubClient) { lines.push(` ${subs.inbox.usage.padEnd(40)}${subs.inbox.description}`, ` ${subs.outbox.usage.padEnd(40)}${subs.outbox.description}`, ` ${subs.read.usage.padEnd(40)}${subs.read.description}`, ` ${subs.archive.usage.padEnd(40)}${subs.archive.description}`, ` ${subs.search.usage.padEnd(40)}${subs.search.description}`); } return lines.join("\n"); } case "inbox": { if (!mailQueryService) { throw "Not available in local mode."; } return mailQueryService.listMessages("received"); } case "outbox": { if (!mailQueryService) { throw "Not available in local mode."; } return mailQueryService.listMessages("sent"); } case "send": { // Expected: ns-mail send "user1,user2" "subject" "message" [file1 file2 ...] if (!argv[1] || !argv[2] || !argv[3]) { throw usageError("send"); } const recipients = userService.resolveUsernames(argv[1]); // Upload any file attachments (argv[4+]) let attachmentIds; let resolvedPaths; const filePaths = argv.slice(4); if (filePaths.length > 0) { resolvedPaths = await shellWrapper.resolvePaths(filePaths); if (hubClient) { attachmentIds = await attachmentService.uploadAll(resolvedPaths, "mail"); } } return sendMessage(recipients, argv[2], argv[3], attachmentIds, resolvedPaths); } case "read": { if (!hubClient) { throw "Not available in local mode."; } const messageId = parseInt(argv[1]); if (isNaN(messageId)) { throw usageError("read"); } const msg = await peekMessage(messageId); await markMessagesRead([msg.id]); return formatMessageDisplay(msg, hubClient.getHubUrl()); } case "archive": { if (!hubClient) { throw "Not available in local mode."; } const messageIds = argv[1]?.split(",").map((id) => parseInt(id.trim())); if (!messageIds || messageIds.length === 0 || messageIds.some(isNaN)) { throw usageError("archive"); } return archiveMessages(messageIds); } case "search": { if (!mailQueryService) { throw "Not available in local mode."; } // Parse flags and search term const searchArgs = argv.slice(1); let includeArchived = false; let subjectOnly = false; const terms = []; for (const arg of searchArgs) { if (arg === "-archived") { includeArchived = true; } else if (arg === "-subject") { subjectOnly = true; } else { terms.push(arg); } } if (terms.length === 0) { throw usageError("search"); } return mailQueryService.searchMessages(terms.join(" "), includeArchived, subjectOnly); } default: { const helpResponse = await handleCommand("help"); const helpContent = typeof helpResponse === "string" ? helpResponse : helpResponse.content; return `Unknown ${mailCmd.name} subcommand '${argv[0]}'. See valid commands below:\n${helpContent}`; } } } async function sendMessage(recipients, subject, message, attachmentIds, resolvedPaths) { if (recipients.length === 0) { throw "No recipients"; } message = message.replace(/\\n/g, "\n"); // Ephemerals always go local; the hub doesn't know about them. const localRecipients = hubClient ? recipients.filter((r) => r.isEphemeral) : recipients; const hubRecipients = hubClient ? recipients.filter((r) => !r.isEphemeral) : []; if (hubRecipients.length > 0) { const response = await hubClient.sendRequest(HubEvents.MAIL_SEND, { fromUserId: localUserId, fromRunId: runService.getRunId(), toUserIds: hubRecipients.map((r) => r.userId), subject, body: message, kind: "mail", attachmentIds, }); if (!response.success) { throw response.error || "Failed to send message"; } } if (localRecipients.length > 0) { const mailContent = { fromUsername: localUsername, fromTitle: localTitle, recipientUsernames: localRecipients.map((r) => r.username), subject, body: message, createdAt: new Date().toISOString(), filePaths: resolvedPaths, }; const display = formatMessageDisplay(mailContent); for (const recipient of localRecipients) { promptNotification.notify({ userId: recipient.userId, wake: "yes", contextOutput: ["New Message:", display], }); } // Auto-start is local-mode only — ephemerals start via `ns-agent create`. if (!hubClient && globalConfig.globalConfig().autoStartAgentsOnMessage) { const runningUserIds = new Set(agentManager.runningAgents.map((a) => a.agentUserId)); for (const recipient of localRecipients) { if (recipient.userId !== localUserId && !recipient.isEphemeral && !runningUserIds.has(recipient.userId)) { agentManager.startAgent(recipient.userId).catch(() => { }); } } } } return "Mail sent"; } async function archiveMessages(messageIds) { if (!hubClient) throw "Not available in local mode."; const response = await hubClient.sendRequest(HubEvents.MAIL_ARCHIVE, { userId: localUserId, messageIds, }); if (!response.success) { throw response.error || "Failed to archive messages"; } return `Messages ${messageIds.join(",")} archived`; } function getAllUserNames() { return userService.getUsers().map((u) => u.username); } function hasMultipleUsers() { return userService.getUsers().length > 1; } /** Highest message ID we've seen from MAIL_UNREAD, used as cursor */ let lastUnreadId = 0; async function getUnreadMessages() { if (!hubClient) { return []; } const response = await hubClient.sendRequest(HubEvents.MAIL_UNREAD, { userId: localUserId, kind: "mail", afterId: lastUnreadId, }); if (!response.success || !response.messages?.length) { return []; } // Advance cursor to max returned ID for (const m of response.messages) { if (m.id > lastUnreadId) { lastUnreadId = m.id; } } return response.messages; } async function peekMessage(messageId) { const response = await hubClient.sendRequest(HubEvents.MAIL_PEEK, { userId: localUserId, messageId, }); if (!response.success || !response.message) { throw response.error || "Failed to read message"; } return response.message; } async function markMessagesRead(messageIds) { if (!hubClient || !messageIds.length) return; await hubClient.sendRequest(HubEvents.MAIL_MARK_READ, { userId: localUserId, runId: runService.getRunId(), messageIds, kind: "mail", }); } /** * Check for new mail and create a notification if there are unread messages. * Uses afterId cursor to avoid duplicate notifications. * (Hub mode only) */ async function checkAndNotify() { if (!hubClient) return; const messages = await getUnreadMessages(); if (!messages.length) return; const messageIds = messages.map((m) => m.id); const contextOutput = messages.flatMap((m) => [ "New Message:", formatMessageDisplay(m, hubClient?.getHubUrl()), ]); promptNotification.notify({ userId: localUserId, wake: "yes", contextOutput, processed: () => markMessagesRead(messageIds), }); } // Set up notification listeners based on mode let cleanupFn; if (hubClient) { // Hub mode: listen for MAIL_RECEIVED push from hub const mailReceivedHandler = (data) => { if (data.kind !== "mail") return; if (data.recipientUserIds.includes(localUserId)) { void checkAndNotify(); } }; hubClient.registerEvent(HubEvents.MAIL_RECEIVED, mailReceivedHandler); cleanupFn = () => { hubClient.unregisterEvent(HubEvents.MAIL_RECEIVED, mailReceivedHandler); }; } else { // Local mode: notifications carry contextOutput directly, no setup needed cleanupFn = () => { }; } function cleanup() { cleanupFn(); } const registrableCommand = { command: mailCmd, handleCommand, }; return { ...registrableCommand, getUnreadMessages, sendMessage, getAllUserNames, hasMultipleUsers, checkAndNotify, cleanup, }; } //# sourceMappingURL=mail.js.map