Improvements

This commit is contained in:
Haakam Aujla 2026-01-27 17:08:20 -08:00
parent cabba2ef91
commit 5f4a7fd917
7 changed files with 51 additions and 92 deletions

View File

@ -8,8 +8,8 @@ import {
type ChannelPlugin, type ChannelPlugin,
} from "clawdbot/plugin-sdk"; } from "clawdbot/plugin-sdk";
import { resolveAgentMailAccount, resolveCredentials } from "./accounts.js"; import { resolveAgentMailAccount } from "./accounts.js";
import { getAgentMailClient } from "./client.js"; import { getAgentMailClient, getResolvedCredentials, NOT_CONFIGURED_ERROR } from "./client.js";
import { AgentMailConfigSchema } from "./config-schema.js"; import { AgentMailConfigSchema } from "./config-schema.js";
import { agentmailOnboardingAdapter } from "./onboarding.js"; import { agentmailOnboardingAdapter } from "./onboarding.js";
import { agentmailOutbound } from "./outbound.js"; import { agentmailOutbound } from "./outbound.js";
@ -70,13 +70,9 @@ export const agentmailPlugin: ChannelPlugin<ResolvedAgentMailAccount> = {
emailAddress: account.inboxId, emailAddress: account.inboxId,
}), }),
resolveAllowFrom: ({ cfg }) => resolveAllowFrom: ({ cfg }) =>
((cfg as CoreConfig).channels?.agentmail?.allowFrom ?? []).map((entry) => ((cfg as CoreConfig).channels?.agentmail?.allowFrom ?? []).map(String),
String(entry)
),
formatAllowFrom: ({ allowFrom }) => formatAllowFrom: ({ allowFrom }) =>
allowFrom allowFrom.map((e) => String(e).toLowerCase().trim()).filter(Boolean),
.map((entry) => String(entry).toLowerCase().trim())
.filter(Boolean),
}, },
security: { security: {
@ -98,12 +94,7 @@ export const agentmailPlugin: ChannelPlugin<ResolvedAgentMailAccount> = {
messaging: { messaging: {
normalizeTarget: (raw) => raw.trim() || undefined, normalizeTarget: (raw) => raw.trim() || undefined,
targetResolver: { targetResolver: {
looksLikeId: (raw) => { looksLikeId: (raw) => /\S+@\S+\.\S+/.test(raw.trim()),
const trimmed = raw.trim();
if (!trimmed) return false;
// Check if it looks like an email
return trimmed.includes("@") && trimmed.includes(".");
},
hint: "<email@example.com>", hint: "<email@example.com>",
}, },
}, },
@ -164,28 +155,15 @@ export const agentmailPlugin: ChannelPlugin<ResolvedAgentMailAccount> = {
probe: s.probe, probe: s.probe,
lastProbeAt: s.lastProbeAt ?? null, lastProbeAt: s.lastProbeAt ?? null,
}), }),
probeAccount: async ({ cfg }) => { probeAccount: async () => {
try { try {
const { apiKey, inboxId } = resolveCredentials(cfg as CoreConfig); const { apiKey, inboxId } = getResolvedCredentials();
if (!apiKey || !inboxId) if (!apiKey || !inboxId) return { ok: false, error: NOT_CONFIGURED_ERROR, elapsedMs: 0 };
return {
ok: false,
error: "Missing token or email address",
elapsedMs: 0,
};
const start = Date.now(); const start = Date.now();
const inbox = await getAgentMailClient(apiKey).inboxes.get(inboxId); const inbox = await getAgentMailClient(apiKey).inboxes.get(inboxId);
return { return { ok: true, elapsedMs: Date.now() - start, meta: { inboxId: inbox.inboxId } };
ok: true,
elapsedMs: Date.now() - start,
meta: { inboxId: inbox.inboxId },
};
} catch (err) { } catch (err) {
return { return { ok: false, error: err instanceof Error ? err.message : String(err), elapsedMs: 0 };
ok: false,
error: err instanceof Error ? err.message : String(err),
elapsedMs: 0,
};
} }
}, },
buildAccountSnapshot: ({ account: a, runtime: r, probe }) => ({ buildAccountSnapshot: ({ account: a, runtime: r, probe }) => ({

View File

@ -4,34 +4,31 @@ import { resolveCredentials } from "./accounts.js";
import { getAgentMailRuntime } from "./runtime.js"; import { getAgentMailRuntime } from "./runtime.js";
import type { CoreConfig } from "./utils.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 sharedClient: AgentMailClient | null = null;
let sharedClientKey: string | 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. */ /** Resolves credentials from current config. */
export function getResolvedCredentials() { export function getResolvedCredentials() {
return resolveCredentials(getAgentMailRuntime().config.loadConfig() as CoreConfig); 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. */ /** Returns client and inboxId, or throws if not configured. */
export function getClientAndInbox(): { client: AgentMailClient; inboxId: string } { export function getClientAndInbox(): { client: AgentMailClient; inboxId: string } {
const { apiKey, inboxId } = getResolvedCredentials(); 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 }; return { client: getAgentMailClient(apiKey), inboxId };
} }

View File

@ -1,7 +1,6 @@
import type { AgentMail } from "agentmail"; import type { AgentMail } from "agentmail";
import { resolveCredentials } from "./accounts.js"; import { getAgentMailClient, getResolvedCredentials, NOT_CONFIGURED_ERROR } from "./client.js";
import { getAgentMailClient } from "./client.js";
import { getAgentMailRuntime } from "./runtime.js"; import { getAgentMailRuntime } from "./runtime.js";
import { checkSenderAllowed, labelMessageAllowed } from "./filtering.js"; import { checkSenderAllowed, labelMessageAllowed } from "./filtering.js";
import { extractMessageBody, fetchFormattedThread } from "./thread.js"; import { extractMessageBody, fetchFormattedThread } from "./thread.js";
@ -54,9 +53,9 @@ export async function monitorAgentMailProvider(
}; };
const accountId = opts.accountId ?? "default"; const accountId = opts.accountId ?? "default";
const { apiKey, inboxId } = resolveCredentials(cfg); const { apiKey, inboxId } = getResolvedCredentials();
if (!apiKey || !inboxId) { if (!apiKey || !inboxId) {
logger.warn("AgentMail not configured (missing token or email address)"); logger.warn(NOT_CONFIGURED_ERROR);
return; return;
} }

View File

@ -1,11 +1,8 @@
import { AgentMailClient } from "agentmail"; import { AgentMailClient } from "agentmail";
import type { import type { ChannelOnboardingAdapter, MoltbotConfig } from "clawdbot/plugin-sdk";
ChannelOnboardingAdapter,
MoltbotConfig,
} from "clawdbot/plugin-sdk";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } 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"; import type { AgentMailConfig, CoreConfig } from "./utils.js";
const channel = "agentmail" as const; const channel = "agentmail" as const;
@ -55,14 +52,13 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
channel, channel,
getStatus: async ({ cfg }) => { getStatus: async ({ cfg }) => {
const { apiKey, inboxId } = resolveCredentials(cfg as CoreConfig); const account = resolveAgentMailAccount({ cfg: cfg as CoreConfig });
const configured = Boolean(apiKey && inboxId);
return { return {
channel, channel,
configured, configured: account.configured,
statusLines: [`AgentMail: ${configured ? `configured (${inboxId})` : "needs token"}`], statusLines: [`AgentMail: ${account.configured ? `configured (${account.inboxId})` : "needs token"}`],
selectionHint: configured ? "configured" : "not configured", selectionHint: account.configured ? "configured" : "not configured",
quickstartScore: configured ? 1 : 5, quickstartScore: account.configured ? 1 : 5,
}; };
}, },
@ -73,9 +69,6 @@ export const agentmailOnboardingAdapter: ChannelOnboardingAdapter = {
let next = cfg as MoltbotConfig; let next = cfg as MoltbotConfig;
const account = resolveAgentMailAccount({ cfg: next as CoreConfig, accountId }); 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()); 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 // 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 already configured, ask to keep
if (configured) { if (account.configured) {
const keep = await prompter.confirm({ const keep = await prompter.confirm({
message: `AgentMail already configured (${inboxId}). Keep current settings?`, message: `AgentMail already configured (${account.inboxId}). Keep current settings?`,
initialValue: true, initialValue: true,
}); });
if (keep) return { cfg: next }; if (keep) return { cfg: next };

View File

@ -4,7 +4,7 @@ import type { ChannelOutboundAdapter } from "clawdbot/plugin-sdk";
import { getClientAndInbox } from "./client.js"; import { getClientAndInbox } from "./client.js";
import { getAgentMailRuntime } from "./runtime.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: { export async function sendAgentMailReply(params: {
client: AgentMailClient; client: AgentMailClient;
inboxId: string; inboxId: string;
@ -12,24 +12,23 @@ export async function sendAgentMailReply(params: {
text: string; text: string;
html?: string; html?: string;
}): Promise<{ messageId: string; threadId: string }> { }): Promise<{ messageId: string; threadId: string }> {
const { client, inboxId, messageId, text, html } = params; return params.client.inboxes.messages.replyAll(params.inboxId, params.messageId, {
return client.inboxes.messages.reply(inboxId, messageId, { text, html }); 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: { async function sendMessage(params: {
to: string; to: string;
text: string; text: string;
html?: string; html?: string;
replyToId?: string; replyToId?: string;
}): Promise<{ channel: "agentmail"; messageId: string; threadId: string }> { }): Promise<{ channel: "agentmail"; messageId: string; threadId: string }> {
const { to, text, html, replyToId } = params;
const { client, inboxId } = getClientAndInbox(); const { client, inboxId } = getClientAndInbox();
const result = params.replyToId
const result = replyToId ? await client.inboxes.messages.replyAll(inboxId, params.replyToId, { text: params.text, html: params.html })
? await client.inboxes.messages.reply(inboxId, replyToId, { text, html }) : await client.inboxes.messages.send(inboxId, { to: [params.to], text: params.text, html: params.html });
: await client.inboxes.messages.send(inboxId, { to: [to], text, html });
return { channel: "agentmail", ...result }; return { channel: "agentmail", ...result };
} }

View File

@ -1,10 +1,9 @@
import type { AgentMailClient, AgentMail } from "agentmail"; import type { AgentMailClient, AgentMail } from "agentmail";
import { formatAttachments } from "./attachment.js"; import { formatAttachments } from "./attachment.js";
import { formatUtcDate } from "./utils.js"; import { formatUtcDate, type Message } from "./utils.js";
type Thread = AgentMail.threads.Thread; type Thread = AgentMail.threads.Thread;
type Message = AgentMail.messages.Message;
/** /**
* Extracts the body text from a message, preferring extractedText. * Extracts the body text from a message, preferring extractedText.

View File

@ -1,4 +1,5 @@
import type { AgentMail } from "agentmail"; import type { AgentMail } from "agentmail";
import type { MoltbotConfig } from "clawdbot/plugin-sdk";
import type { z } from "zod"; import type { z } from "zod";
import type { AgentMailConfigSchema } from "./config-schema.js"; import type { AgentMailConfigSchema } from "./config-schema.js";
@ -6,16 +7,9 @@ import type { AgentMailConfigSchema } from "./config-schema.js";
/** AgentMail channel configuration. */ /** AgentMail channel configuration. */
export type AgentMailConfig = z.infer<typeof AgentMailConfigSchema>; export type AgentMailConfig = z.infer<typeof AgentMailConfigSchema>;
/** Core config shape with AgentMail channel. */ /** Core config with AgentMail channel typed. */
export type CoreConfig = { export type CoreConfig = MoltbotConfig & {
channels?: { channels?: { agentmail?: AgentMailConfig };
agentmail?: AgentMailConfig;
};
session?: {
store?: string;
[key: string]: unknown;
};
[key: string]: unknown;
}; };
/** Resolved AgentMail account with runtime state. */ /** Resolved AgentMail account with runtime state. */