diff --git a/extensions/agentmail/src/channel.ts b/extensions/agentmail/src/channel.ts index 8cc272621..f64b7d2b5 100644 --- a/extensions/agentmail/src/channel.ts +++ b/extensions/agentmail/src/channel.ts @@ -8,8 +8,8 @@ import { type ChannelPlugin, } from "clawdbot/plugin-sdk"; -import { resolveAgentMailAccount, resolveCredentials } from "./accounts.js"; -import { getAgentMailClient } from "./client.js"; +import { resolveAgentMailAccount } from "./accounts.js"; +import { getAgentMailClient, getResolvedCredentials, NOT_CONFIGURED_ERROR } from "./client.js"; import { AgentMailConfigSchema } from "./config-schema.js"; import { agentmailOnboardingAdapter } from "./onboarding.js"; import { agentmailOutbound } from "./outbound.js"; @@ -70,13 +70,9 @@ export const agentmailPlugin: ChannelPlugin = { emailAddress: account.inboxId, }), resolveAllowFrom: ({ cfg }) => - ((cfg as CoreConfig).channels?.agentmail?.allowFrom ?? []).map((entry) => - String(entry) - ), + ((cfg as CoreConfig).channels?.agentmail?.allowFrom ?? []).map(String), formatAllowFrom: ({ allowFrom }) => - allowFrom - .map((entry) => String(entry).toLowerCase().trim()) - .filter(Boolean), + allowFrom.map((e) => String(e).toLowerCase().trim()).filter(Boolean), }, security: { @@ -98,12 +94,7 @@ export const agentmailPlugin: ChannelPlugin = { messaging: { normalizeTarget: (raw) => raw.trim() || undefined, targetResolver: { - looksLikeId: (raw) => { - const trimmed = raw.trim(); - if (!trimmed) return false; - // Check if it looks like an email - return trimmed.includes("@") && trimmed.includes("."); - }, + looksLikeId: (raw) => /\S+@\S+\.\S+/.test(raw.trim()), hint: "", }, }, @@ -164,28 +155,15 @@ export const agentmailPlugin: ChannelPlugin = { probe: s.probe, lastProbeAt: s.lastProbeAt ?? null, }), - probeAccount: async ({ cfg }) => { + probeAccount: async () => { try { - const { apiKey, inboxId } = resolveCredentials(cfg as CoreConfig); - if (!apiKey || !inboxId) - return { - ok: false, - error: "Missing token or email address", - elapsedMs: 0, - }; + const { apiKey, inboxId } = getResolvedCredentials(); + if (!apiKey || !inboxId) return { ok: false, error: NOT_CONFIGURED_ERROR, elapsedMs: 0 }; const start = Date.now(); const inbox = await getAgentMailClient(apiKey).inboxes.get(inboxId); - return { - ok: true, - elapsedMs: Date.now() - start, - meta: { inboxId: inbox.inboxId }, - }; + return { ok: true, elapsedMs: Date.now() - start, meta: { inboxId: inbox.inboxId } }; } catch (err) { - return { - ok: false, - error: err instanceof Error ? err.message : String(err), - elapsedMs: 0, - }; + return { ok: false, error: err instanceof Error ? err.message : String(err), elapsedMs: 0 }; } }, buildAccountSnapshot: ({ account: a, runtime: r, probe }) => ({ diff --git a/extensions/agentmail/src/client.ts b/extensions/agentmail/src/client.ts index d08b5490e..cc9fdb8b6 100644 --- a/extensions/agentmail/src/client.ts +++ b/extensions/agentmail/src/client.ts @@ -4,34 +4,31 @@ import { resolveCredentials } from "./accounts.js"; import { getAgentMailRuntime } from "./runtime.js"; import type { CoreConfig } from "./utils.js"; +export const NOT_CONFIGURED_ERROR = "AgentMail not configured (missing token or email address)"; + let sharedClient: AgentMailClient | null = null; let sharedClientKey: string | null = null; -/** Creates or returns a shared AgentMailClient instance. Recreates if key changed. */ -export function getAgentMailClient(apiKey?: string): AgentMailClient { - const key = apiKey ?? getResolvedCredentials().apiKey; - if (!key) throw new Error("AgentMail token is required"); - - // Return cached client if key matches - if (sharedClient && sharedClientKey === key) { - return sharedClient; - } - - // Create new client (key changed or first call) - sharedClient = new AgentMailClient({ apiKey: key }); - sharedClientKey = key; - return sharedClient; -} - /** Resolves credentials from current config. */ export function getResolvedCredentials() { return resolveCredentials(getAgentMailRuntime().config.loadConfig() as CoreConfig); } +/** Creates or returns a shared AgentMailClient instance. Recreates if key changed. */ +export function getAgentMailClient(apiKey?: string): AgentMailClient { + const key = apiKey ?? getResolvedCredentials().apiKey; + if (!key) throw new Error("AgentMail token is required"); + + if (sharedClient && sharedClientKey === key) return sharedClient; + + sharedClient = new AgentMailClient({ apiKey: key }); + sharedClientKey = key; + return sharedClient; +} + /** Returns client and inboxId, or throws if not configured. */ export function getClientAndInbox(): { client: AgentMailClient; inboxId: string } { const { apiKey, inboxId } = getResolvedCredentials(); - if (!apiKey || !inboxId) throw new Error("AgentMail not configured (missing token or email address)"); + if (!apiKey || !inboxId) throw new Error(NOT_CONFIGURED_ERROR); return { client: getAgentMailClient(apiKey), inboxId }; } - diff --git a/extensions/agentmail/src/monitor.ts b/extensions/agentmail/src/monitor.ts index 1283aac81..e28e639a1 100644 --- a/extensions/agentmail/src/monitor.ts +++ b/extensions/agentmail/src/monitor.ts @@ -1,7 +1,6 @@ import type { AgentMail } from "agentmail"; -import { resolveCredentials } from "./accounts.js"; -import { getAgentMailClient } from "./client.js"; +import { getAgentMailClient, getResolvedCredentials, NOT_CONFIGURED_ERROR } from "./client.js"; import { getAgentMailRuntime } from "./runtime.js"; import { checkSenderAllowed, labelMessageAllowed } from "./filtering.js"; import { extractMessageBody, fetchFormattedThread } from "./thread.js"; @@ -54,9 +53,9 @@ export async function monitorAgentMailProvider( }; const accountId = opts.accountId ?? "default"; - const { apiKey, inboxId } = resolveCredentials(cfg); + const { apiKey, inboxId } = getResolvedCredentials(); if (!apiKey || !inboxId) { - logger.warn("AgentMail not configured (missing token or email address)"); + logger.warn(NOT_CONFIGURED_ERROR); return; } diff --git a/extensions/agentmail/src/onboarding.ts b/extensions/agentmail/src/onboarding.ts index 079a05d13..176111193 100644 --- a/extensions/agentmail/src/onboarding.ts +++ b/extensions/agentmail/src/onboarding.ts @@ -1,11 +1,8 @@ import { AgentMailClient } from "agentmail"; -import type { - ChannelOnboardingAdapter, - MoltbotConfig, -} from "clawdbot/plugin-sdk"; +import type { ChannelOnboardingAdapter, MoltbotConfig } from "clawdbot/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk"; -import { resolveAgentMailAccount, resolveCredentials } from "./accounts.js"; +import { resolveAgentMailAccount } from "./accounts.js"; import type { AgentMailConfig, CoreConfig } from "./utils.js"; const channel = "agentmail" as const; @@ -55,14 +52,13 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = { channel, getStatus: async ({ cfg }) => { - const { apiKey, inboxId } = resolveCredentials(cfg as CoreConfig); - const configured = Boolean(apiKey && inboxId); + const account = resolveAgentMailAccount({ cfg: cfg as CoreConfig }); return { channel, - configured, - statusLines: [`AgentMail: ${configured ? `configured (${inboxId})` : "needs token"}`], - selectionHint: configured ? "configured" : "not configured", - quickstartScore: configured ? 1 : 5, + configured: account.configured, + statusLines: [`AgentMail: ${account.configured ? `configured (${account.inboxId})` : "needs token"}`], + selectionHint: account.configured ? "configured" : "not configured", + quickstartScore: account.configured ? 1 : 5, }; }, @@ -73,9 +69,6 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = { let next = cfg as MoltbotConfig; const account = resolveAgentMailAccount({ cfg: next as CoreConfig, accountId }); - const { apiKey, inboxId } = resolveCredentials(next as CoreConfig); - const configured = Boolean(apiKey && inboxId); - const canUseEnv = accountId === DEFAULT_ACCOUNT_ID && Boolean(process.env.AGENTMAIL_TOKEN?.trim()); // If env var token is available and not already configured, offer to use it @@ -92,9 +85,9 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = { } // If already configured, ask to keep - if (configured) { + if (account.configured) { const keep = await prompter.confirm({ - message: `AgentMail already configured (${inboxId}). Keep current settings?`, + message: `AgentMail already configured (${account.inboxId}). Keep current settings?`, initialValue: true, }); if (keep) return { cfg: next }; diff --git a/extensions/agentmail/src/outbound.ts b/extensions/agentmail/src/outbound.ts index b6a003253..fc416032f 100644 --- a/extensions/agentmail/src/outbound.ts +++ b/extensions/agentmail/src/outbound.ts @@ -4,7 +4,7 @@ import type { ChannelOutboundAdapter } from "clawdbot/plugin-sdk"; import { getClientAndInbox } from "./client.js"; import { getAgentMailRuntime } from "./runtime.js"; -/** Sends a reply to an email message via AgentMail. */ +/** Sends a reply-all to an email message via AgentMail. */ export async function sendAgentMailReply(params: { client: AgentMailClient; inboxId: string; @@ -12,24 +12,23 @@ export async function sendAgentMailReply(params: { text: string; html?: string; }): Promise<{ messageId: string; threadId: string }> { - const { client, inboxId, messageId, text, html } = params; - return client.inboxes.messages.reply(inboxId, messageId, { text, html }); + return params.client.inboxes.messages.replyAll(params.inboxId, params.messageId, { + text: params.text, + html: params.html, + }); } -/** Sends a message (reply or new) and returns standardized result. */ +/** Sends a message (reply-all or new) and returns standardized result. */ async function sendMessage(params: { to: string; text: string; html?: string; replyToId?: string; }): Promise<{ channel: "agentmail"; messageId: string; threadId: string }> { - const { to, text, html, replyToId } = params; const { client, inboxId } = getClientAndInbox(); - - const result = replyToId - ? await client.inboxes.messages.reply(inboxId, replyToId, { text, html }) - : await client.inboxes.messages.send(inboxId, { to: [to], text, html }); - + const result = params.replyToId + ? await client.inboxes.messages.replyAll(inboxId, params.replyToId, { text: params.text, html: params.html }) + : await client.inboxes.messages.send(inboxId, { to: [params.to], text: params.text, html: params.html }); return { channel: "agentmail", ...result }; } diff --git a/extensions/agentmail/src/thread.ts b/extensions/agentmail/src/thread.ts index 30f603c3d..98de56491 100644 --- a/extensions/agentmail/src/thread.ts +++ b/extensions/agentmail/src/thread.ts @@ -1,10 +1,9 @@ import type { AgentMailClient, AgentMail } from "agentmail"; import { formatAttachments } from "./attachment.js"; -import { formatUtcDate } from "./utils.js"; +import { formatUtcDate, type Message } from "./utils.js"; type Thread = AgentMail.threads.Thread; -type Message = AgentMail.messages.Message; /** * Extracts the body text from a message, preferring extractedText. diff --git a/extensions/agentmail/src/utils.ts b/extensions/agentmail/src/utils.ts index f0de25833..152da512e 100644 --- a/extensions/agentmail/src/utils.ts +++ b/extensions/agentmail/src/utils.ts @@ -1,4 +1,5 @@ import type { AgentMail } from "agentmail"; +import type { MoltbotConfig } from "clawdbot/plugin-sdk"; import type { z } from "zod"; import type { AgentMailConfigSchema } from "./config-schema.js"; @@ -6,16 +7,9 @@ import type { AgentMailConfigSchema } from "./config-schema.js"; /** AgentMail channel configuration. */ export type AgentMailConfig = z.infer; -/** Core config shape with AgentMail channel. */ -export type CoreConfig = { - channels?: { - agentmail?: AgentMailConfig; - }; - session?: { - store?: string; - [key: string]: unknown; - }; - [key: string]: unknown; +/** Core config with AgentMail channel typed. */ +export type CoreConfig = MoltbotConfig & { + channels?: { agentmail?: AgentMailConfig }; }; /** Resolved AgentMail account with runtime state. */