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,
} 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<ResolvedAgentMailAccount> = {
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<ResolvedAgentMailAccount> = {
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: "<email@example.com>",
},
},
@ -164,28 +155,15 @@ export const agentmailPlugin: ChannelPlugin<ResolvedAgentMailAccount> = {
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 }) => ({

View File

@ -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 };
}

View File

@ -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;
}

View File

@ -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 };

View File

@ -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 };
}

View File

@ -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.

View File

@ -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<typeof AgentMailConfigSchema>;
/** 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. */