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
From npm:
```bash
clawdbot plugins install @clawdbot/agentmail
moltbot plugins install @moltbot/agentmail
```
From local checkout:
```bash
clawdbot plugins install ./extensions/agentmail
moltbot plugins install ./extensions/agentmail
```
## 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 { agentmailPlugin } from "./src/channel.js";
@ -9,7 +9,7 @@ const plugin = {
name: "AgentMail",
description: "Email channel plugin via AgentMail API",
configSchema: emptyPluginConfigSchema(),
register(api: ClawdbotPluginApi) {
register(api: MoltbotPluginApi) {
setAgentMailRuntime(api.runtime);
api.registerChannel({ plugin: agentmailPlugin });
},

View File

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

View File

@ -1,6 +1,10 @@
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.
@ -33,14 +37,15 @@ export type ResolvedAgentMailCredentials = {
*/
export function resolveCredentials(
cfg: CoreConfig,
env: Record<string, string | undefined> = process.env,
env: Record<string, string | undefined> = process.env
): ResolvedAgentMailCredentials {
const base = cfg.channels?.agentmail ?? {};
return {
apiKey: base.token || env.AGENTMAIL_TOKEN,
inboxId: base.emailAddress || env.AGENTMAIL_EMAIL_ADDRESS,
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 }) => {
if (input.useEnv) return null;
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;
},
applyAccountConfig: ({ cfg, input }) => {
@ -152,11 +153,17 @@ export const agentmailPlugin: ChannelPlugin<ResolvedAgentMailAccount> = {
agentmail: {
...existing,
enabled: true,
...(input.useEnv ? {} : {
...(input.token?.trim() ? { token: input.token.trim() } : {}),
...(input.emailAddress?.trim() ? { emailAddress: input.emailAddress.trim() } : {}),
...(input.webhookPath?.trim() ? { webhookPath: input.webhookPath.trim() } : {}),
}),
...(input.useEnv
? {}
: {
...(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
const runtimeState = new Map<string, {
running: boolean;
lastStartAt: number | null;
lastStopAt: number | null;
lastError: string | null;
lastInboundAt?: number | null;
lastOutboundAt?: number | null;
}>();
const runtimeState = new Map<
string,
{
running: boolean;
lastStartAt: number | null;
lastStopAt: 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}`;
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) {
@ -47,7 +61,9 @@ function sendJson(res: ServerResponse, status: number, data: unknown) {
}
/** 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` : "";
return `${subject}${extractMessageBody(message)}`;
}
@ -98,7 +114,11 @@ export async function monitorAgentMailProvider(
const allowlist = agentmailConfig?.allowlist ?? [];
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}`);
/**

View File

@ -1,7 +1,7 @@
import { AgentMailClient } from "agentmail";
import type {
ChannelOnboardingAdapter,
ClawdbotConfig,
MoltbotConfig,
} 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. */
export function updateAgentMailConfig(
cfg: ClawdbotConfig,
cfg: MoltbotConfig,
updates: Partial<AgentMailConfig>
): ClawdbotConfig {
): MoltbotConfig {
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
const agentmail = (channels.agentmail ?? {}) as AgentMailConfig;
return {
@ -65,7 +65,7 @@ export function updateAgentMailConfig(
...updates,
},
},
} as ClawdbotConfig;
} as MoltbotConfig;
}
export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
@ -93,7 +93,7 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
? normalizeAccountId(accountOverrides.agentmail)
: defaultAccountId;
let next = cfg as ClawdbotConfig;
let next = cfg as MoltbotConfig;
const account = resolveAgentMailAccount({
cfg: next as CoreConfig,
accountId,
@ -164,7 +164,8 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
// Ask if gateway has a public URL
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,
});
@ -180,13 +181,18 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
validate: (v) => {
const trimmed = v?.trim();
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 undefined;
},
})
).trim().replace(/\/+$/, ""); // Remove trailing slashes
)
.trim()
.replace(/\/+$/, ""); // Remove trailing slashes
// Ask for webhook path
const customPath = await prompter.confirm({
@ -214,7 +220,7 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
await client.webhooks.create({
url: `${baseUrl}${webhookPath}`,
eventTypes: ["message.received"],
clientId: `clawdbot-${emailAddress}`, // Idempotent per inbox
clientId: `moltbot-${emailAddress}`, // Idempotent per inbox
});
await prompter.note(
`Webhook registered: ${baseUrl}${webhookPath}`,
@ -266,7 +272,8 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
if (addAllowlist) {
const entry = String(
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();
@ -306,7 +313,7 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
" Event: message.received",
"",
"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"),
"Webhook Setup Required"
);

View File

@ -36,14 +36,20 @@ async function sendMessage(params: {
/** Outbound adapter for the AgentMail channel. */
export const agentmailOutbound: ChannelOutboundAdapter = {
deliveryMode: "direct",
chunker: (text, limit) => getAgentMailRuntime().channel.text.chunkMarkdownText(text, limit),
chunker: (text, limit) =>
getAgentMailRuntime().channel.text.chunkMarkdownText(text, limit),
chunkerMode: "markdown",
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 }) => {
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. */
export type ResolvedAgentMailAccount = {
accountId: string;
/** Account name for identifying this configuration. */
name?: string;
enabled: boolean;
configured: boolean;