This commit is contained in:
Haakam Aujla 2026-01-27 11:06:15 -08:00
parent 1928077e02
commit 22eb3d52a1
9 changed files with 90 additions and 46 deletions

View File

@ -1,19 +1,19 @@
# @clawdbot/agentmail # @moltbot/agentmail
Email channel plugin for Clawdbot via [AgentMail](https://agentmail.to). Email channel plugin for Moltbot via [AgentMail](https://agentmail.to).
## Installation ## Installation
From npm: From npm:
```bash ```bash
clawdbot plugins install @clawdbot/agentmail moltbot plugins install @moltbot/agentmail
``` ```
From local checkout: From local checkout:
```bash ```bash
clawdbot plugins install ./extensions/agentmail moltbot plugins install ./extensions/agentmail
``` ```
## Configuration ## Configuration

View File

@ -1,4 +1,4 @@
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk"; import type { MoltbotPluginApi } from "clawdbot/plugin-sdk";
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk"; import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
import { agentmailPlugin } from "./src/channel.js"; import { agentmailPlugin } from "./src/channel.js";
@ -9,7 +9,7 @@ const plugin = {
name: "AgentMail", name: "AgentMail",
description: "Email channel plugin via AgentMail API", description: "Email channel plugin via AgentMail API",
configSchema: emptyPluginConfigSchema(), configSchema: emptyPluginConfigSchema(),
register(api: ClawdbotPluginApi) { register(api: MoltbotPluginApi) {
setAgentMailRuntime(api.runtime); setAgentMailRuntime(api.runtime);
api.registerChannel({ plugin: agentmailPlugin }); api.registerChannel({ plugin: agentmailPlugin });
}, },

View File

@ -1,9 +1,9 @@
{ {
"name": "@clawdbot/agentmail", "name": "@moltbot/agentmail",
"version": "2026.1.26", "version": "2026.1.26",
"type": "module", "type": "module",
"description": "Clawdbot AgentMail email channel plugin", "description": "Moltbot AgentMail email channel plugin",
"clawdbot": { "moltbot": {
"extensions": [ "extensions": [
"./index.ts" "./index.ts"
], ],
@ -18,7 +18,7 @@
"quickstartAllowFrom": true "quickstartAllowFrom": true
}, },
"install": { "install": {
"npmSpec": "@clawdbot/agentmail", "npmSpec": "@moltbot/agentmail",
"localPath": "extensions/agentmail", "localPath": "extensions/agentmail",
"defaultChoice": "npm" "defaultChoice": "npm"
} }

View File

@ -1,6 +1,10 @@
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk";
import type { AgentMailConfig, CoreConfig, ResolvedAgentMailAccount } from "./utils.js"; import type {
AgentMailConfig,
CoreConfig,
ResolvedAgentMailAccount,
} from "./utils.js";
/** /**
* Lists all AgentMail account IDs. * Lists all AgentMail account IDs.
@ -33,14 +37,15 @@ export type ResolvedAgentMailCredentials = {
*/ */
export function resolveCredentials( export function resolveCredentials(
cfg: CoreConfig, cfg: CoreConfig,
env: Record<string, string | undefined> = process.env, env: Record<string, string | undefined> = process.env
): ResolvedAgentMailCredentials { ): ResolvedAgentMailCredentials {
const base = cfg.channels?.agentmail ?? {}; const base = cfg.channels?.agentmail ?? {};
return { return {
apiKey: base.token || env.AGENTMAIL_TOKEN, apiKey: base.token || env.AGENTMAIL_TOKEN,
inboxId: base.emailAddress || env.AGENTMAIL_EMAIL_ADDRESS, inboxId: base.emailAddress || env.AGENTMAIL_EMAIL_ADDRESS,
webhookUrl: base.webhookUrl || env.AGENTMAIL_WEBHOOK_URL, webhookUrl: base.webhookUrl || env.AGENTMAIL_WEBHOOK_URL,
webhookPath: base.webhookPath || env.AGENTMAIL_WEBHOOK_PATH || "/webhooks/agentmail", webhookPath:
base.webhookPath || env.AGENTMAIL_WEBHOOK_PATH || "/webhooks/agentmail",
}; };
} }

View File

@ -140,7 +140,8 @@ export const agentmailPlugin: ChannelPlugin<ResolvedAgentMailAccount> = {
validateInput: ({ input }) => { validateInput: ({ input }) => {
if (input.useEnv) return null; if (input.useEnv) return null;
if (!input.token?.trim()) return "AgentMail requires --token"; if (!input.token?.trim()) return "AgentMail requires --token";
if (!input.emailAddress?.trim()) return "AgentMail requires --email-address"; if (!input.emailAddress?.trim())
return "AgentMail requires --email-address";
return null; return null;
}, },
applyAccountConfig: ({ cfg, input }) => { applyAccountConfig: ({ cfg, input }) => {
@ -152,11 +153,17 @@ export const agentmailPlugin: ChannelPlugin<ResolvedAgentMailAccount> = {
agentmail: { agentmail: {
...existing, ...existing,
enabled: true, enabled: true,
...(input.useEnv ? {} : { ...(input.useEnv
...(input.token?.trim() ? { token: input.token.trim() } : {}), ? {}
...(input.emailAddress?.trim() ? { emailAddress: input.emailAddress.trim() } : {}), : {
...(input.webhookPath?.trim() ? { webhookPath: input.webhookPath.trim() } : {}), ...(input.token?.trim() ? { token: input.token.trim() } : {}),
}), ...(input.emailAddress?.trim()
? { emailAddress: input.emailAddress.trim() }
: {}),
...(input.webhookPath?.trim()
? { webhookPath: input.webhookPath.trim() }
: {}),
}),
}, },
}, },
}; };

View File

@ -16,18 +16,32 @@ export type MonitorAgentMailOptions = {
}; };
// Runtime state tracking // Runtime state tracking
const runtimeState = new Map<string, { const runtimeState = new Map<
running: boolean; string,
lastStartAt: number | null; {
lastStopAt: number | null; running: boolean;
lastError: string | null; lastStartAt: number | null;
lastInboundAt?: number | null; lastStopAt: number | null;
lastOutboundAt?: number | null; lastError: string | null;
}>(); lastInboundAt?: number | null;
lastOutboundAt?: number | null;
}
>();
function recordState(accountId: string, state: Partial<typeof runtimeState extends Map<string, infer V> ? V : never>) { function recordState(
accountId: string,
state: Partial<typeof runtimeState extends Map<string, infer V> ? V : never>
) {
const key = `agentmail:${accountId}`; const key = `agentmail:${accountId}`;
runtimeState.set(key, { ...runtimeState.get(key) ?? { running: false, lastStartAt: null, lastStopAt: null, lastError: null }, ...state }); runtimeState.set(key, {
...(runtimeState.get(key) ?? {
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
}),
...state,
});
} }
export function getAgentMailRuntimeState(accountId: string) { export function getAgentMailRuntimeState(accountId: string) {
@ -47,7 +61,9 @@ function sendJson(res: ServerResponse, status: number, data: unknown) {
} }
/** Builds message body from webhook payload as fallback when API fetch fails. */ /** Builds message body from webhook payload as fallback when API fetch fails. */
function buildFallbackBody(message: NonNullable<MessageReceivedEvent["message"]>): string { function buildFallbackBody(
message: NonNullable<MessageReceivedEvent["message"]>
): string {
const subject = message.subject ? `Subject: ${message.subject}\n\n` : ""; const subject = message.subject ? `Subject: ${message.subject}\n\n` : "";
return `${subject}${extractMessageBody(message)}`; return `${subject}${extractMessageBody(message)}`;
} }
@ -98,7 +114,11 @@ export async function monitorAgentMailProvider(
const allowlist = agentmailConfig?.allowlist ?? []; const allowlist = agentmailConfig?.allowlist ?? [];
const blocklist = agentmailConfig?.blocklist ?? []; const blocklist = agentmailConfig?.blocklist ?? [];
recordState(accountId, { running: true, lastStartAt: Date.now(), lastError: null }); recordState(accountId, {
running: true,
lastStartAt: Date.now(),
lastError: null,
});
logger.info(`AgentMail: starting monitor for ${inboxId}`); logger.info(`AgentMail: starting monitor for ${inboxId}`);
/** /**

View File

@ -1,7 +1,7 @@
import { AgentMailClient } from "agentmail"; import { AgentMailClient } from "agentmail";
import type { import type {
ChannelOnboardingAdapter, ChannelOnboardingAdapter,
ClawdbotConfig, MoltbotConfig,
} from "clawdbot/plugin-sdk"; } from "clawdbot/plugin-sdk";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk"; import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "clawdbot/plugin-sdk";
@ -51,9 +51,9 @@ async function listInboxes(client: AgentMailClient): Promise<string[]> {
/** Helper to build config with agentmail channel updates. */ /** Helper to build config with agentmail channel updates. */
export function updateAgentMailConfig( export function updateAgentMailConfig(
cfg: ClawdbotConfig, cfg: MoltbotConfig,
updates: Partial<AgentMailConfig> updates: Partial<AgentMailConfig>
): ClawdbotConfig { ): MoltbotConfig {
const channels = (cfg.channels ?? {}) as Record<string, unknown>; const channels = (cfg.channels ?? {}) as Record<string, unknown>;
const agentmail = (channels.agentmail ?? {}) as AgentMailConfig; const agentmail = (channels.agentmail ?? {}) as AgentMailConfig;
return { return {
@ -65,7 +65,7 @@ export function updateAgentMailConfig(
...updates, ...updates,
}, },
}, },
} as ClawdbotConfig; } as MoltbotConfig;
} }
export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = { export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
@ -93,7 +93,7 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
? normalizeAccountId(accountOverrides.agentmail) ? normalizeAccountId(accountOverrides.agentmail)
: defaultAccountId; : defaultAccountId;
let next = cfg as ClawdbotConfig; let next = cfg as MoltbotConfig;
const account = resolveAgentMailAccount({ const account = resolveAgentMailAccount({
cfg: next as CoreConfig, cfg: next as CoreConfig,
accountId, accountId,
@ -164,7 +164,8 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
// Ask if gateway has a public URL // Ask if gateway has a public URL
const hasPublicUrl = await prompter.confirm({ const hasPublicUrl = await prompter.confirm({
message: "Does your gateway have a public URL? (for automatic webhook setup)", message:
"Does your gateway have a public URL? (for automatic webhook setup)",
initialValue: false, initialValue: false,
}); });
@ -180,13 +181,18 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
validate: (v) => { validate: (v) => {
const trimmed = v?.trim(); const trimmed = v?.trim();
if (!trimmed) return "Required"; if (!trimmed) return "Required";
if (!trimmed.startsWith("http://") && !trimmed.startsWith("https://")) { if (
!trimmed.startsWith("http://") &&
!trimmed.startsWith("https://")
) {
return "Must start with http:// or https://"; return "Must start with http:// or https://";
} }
return undefined; return undefined;
}, },
}) })
).trim().replace(/\/+$/, ""); // Remove trailing slashes )
.trim()
.replace(/\/+$/, ""); // Remove trailing slashes
// Ask for webhook path // Ask for webhook path
const customPath = await prompter.confirm({ const customPath = await prompter.confirm({
@ -214,7 +220,7 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
await client.webhooks.create({ await client.webhooks.create({
url: `${baseUrl}${webhookPath}`, url: `${baseUrl}${webhookPath}`,
eventTypes: ["message.received"], eventTypes: ["message.received"],
clientId: `clawdbot-${emailAddress}`, // Idempotent per inbox clientId: `moltbot-${emailAddress}`, // Idempotent per inbox
}); });
await prompter.note( await prompter.note(
`Webhook registered: ${baseUrl}${webhookPath}`, `Webhook registered: ${baseUrl}${webhookPath}`,
@ -266,7 +272,8 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
if (addAllowlist) { if (addAllowlist) {
const entry = String( const entry = String(
await prompter.text({ await prompter.text({
message: "Email or domain to allow (e.g., user@example.com or example.com)", message:
"Email or domain to allow (e.g., user@example.com or example.com)",
}) })
).trim(); ).trim();
@ -306,7 +313,7 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
" Event: message.received", " Event: message.received",
"", "",
"The gateway must be publicly accessible for webhooks to work.", "The gateway must be publicly accessible for webhooks to work.",
"Without this, Clawdbot won't receive incoming emails.", "Without this, Moltbot won't receive incoming emails.",
].join("\n"), ].join("\n"),
"Webhook Setup Required" "Webhook Setup Required"
); );

View File

@ -36,14 +36,20 @@ async function sendMessage(params: {
/** Outbound adapter for the AgentMail channel. */ /** Outbound adapter for the AgentMail channel. */
export const agentmailOutbound: ChannelOutboundAdapter = { export const agentmailOutbound: ChannelOutboundAdapter = {
deliveryMode: "direct", deliveryMode: "direct",
chunker: (text, limit) => getAgentMailRuntime().channel.text.chunkMarkdownText(text, limit), chunker: (text, limit) =>
getAgentMailRuntime().channel.text.chunkMarkdownText(text, limit),
chunkerMode: "markdown", chunkerMode: "markdown",
textChunkLimit: 4000, textChunkLimit: 4000,
sendText: ({ to, text, replyToId }) => sendMessage({ to, text, replyToId: replyToId ?? undefined }), sendText: ({ to, text, replyToId }) =>
sendMessage({ to, text, replyToId: replyToId ?? undefined }),
sendMedia: ({ to, text, mediaUrl, replyToId }) => { sendMedia: ({ to, text, mediaUrl, replyToId }) => {
const fullText = mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text; const fullText = mediaUrl ? `${text}\n\nAttachment: ${mediaUrl}` : text;
return sendMessage({ to, text: fullText, replyToId: replyToId ?? undefined }); return sendMessage({
to,
text: fullText,
replyToId: replyToId ?? undefined,
});
}, },
}; };

View File

@ -21,7 +21,6 @@ export type CoreConfig = {
/** Resolved AgentMail account with runtime state. */ /** Resolved AgentMail account with runtime state. */
export type ResolvedAgentMailAccount = { export type ResolvedAgentMailAccount = {
accountId: string; accountId: string;
/** Account name for identifying this configuration. */
name?: string; name?: string;
enabled: boolean; enabled: boolean;
configured: boolean; configured: boolean;