fix(xmtp): show tag in channel selection and make allowFrom optional for pairing

Bug 1: Missing tag in channel list
- Changed selectionLabel from 'XMTP' to 'XMTP (Decentralized Messaging)'
- Updated both channel.ts and types.xmtp.ts to match package.json

Bug 2: allowFrom incorrectly required in pairing mode
- Removed forceAllowFrom condition from allowFrom prompt
- Now only prompts for allowFrom when dmPolicy === 'allowlist'
- Pairing mode correctly allows anyone to message (approved later)
This commit is contained in:
HeresMyGit 2026-01-26 18:38:27 -08:00
parent 047070a5cf
commit 893d1e60e9
3 changed files with 2413 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,752 @@
/**
* XMTP Onboarding Adapter
*
* Provides the CLI onboarding wizard for configuring XMTP channel.
* Follows the pattern established by Telegram, Discord, and Signal adapters.
*/
import { randomBytes } from "crypto";
import { createUser } from "@xmtp/agent-sdk/user";
import {
DEFAULT_XMTP_ACCOUNT_ID,
listXmtpAccountIds,
normalizeXmtpAccountId,
resolveDefaultXmtpAccountId,
resolveXmtpAccount,
} from "./accounts.js";
import type { XmtpConfig, XmtpDmPolicy, XmtpEnv } from "./types.xmtp.js";
// ============================================================================
// Types
// ============================================================================
/**
* Prompter interface (from clawdbot onboarding system).
* Matches the @clack/prompts interface used by clawdbot.
*/
interface Prompter {
text: (options: {
message: string;
placeholder?: string;
initialValue?: string;
validate?: (value: string | undefined) => string | undefined;
}) => Promise<string | symbol>;
confirm: (options: {
message: string;
initialValue?: boolean;
}) => Promise<boolean | symbol>;
select: <T>(options: {
message: string;
options: Array<{ value: T; label: string; hint?: string }>;
initialValue?: T;
}) => Promise<T | symbol>;
note: (message: string, title?: string) => Promise<void>;
}
/**
* Status result from getStatus.
*/
interface OnboardingStatus {
provider: string;
configured: boolean;
statusLines: string[];
selectionHint: string;
quickstartScore: number;
}
/**
* Configure result.
*/
interface ConfigureResult {
cfg: Record<string, unknown>;
accountId: string;
}
/**
* DM policy adapter interface.
*/
interface DmPolicyAdapter {
label: string;
provider: string;
policyKey: string;
allowFromKey: string;
getCurrent: (cfg: Record<string, unknown>) => XmtpDmPolicy;
setPolicy: (cfg: Record<string, unknown>, policy: XmtpDmPolicy) => Record<string, unknown>;
}
/**
* Onboarding adapter interface.
*/
export interface XmtpOnboardingAdapter {
provider: string;
getStatus: (params: {
cfg: Record<string, unknown>;
options?: Record<string, unknown>;
accountOverrides?: Record<string, string>;
}) => Promise<OnboardingStatus>;
configure: (params: {
cfg: Record<string, unknown>;
prompter: Prompter;
accountOverrides: Record<string, string>;
shouldPromptAccountIds?: boolean;
forceAllowFrom?: boolean;
options?: Record<string, unknown>;
}) => Promise<ConfigureResult>;
dmPolicy: DmPolicyAdapter;
disable: (cfg: Record<string, unknown>) => Record<string, unknown>;
}
// ============================================================================
// Wallet Generation
// ============================================================================
/**
* Generated wallet result.
*/
export interface GeneratedWallet {
/** Private key (0x-prefixed, 64 hex chars) */
privateKey: `0x${string}`;
/** Derived Ethereum address */
address: string;
}
/**
* Generate a new Ethereum wallet for XMTP bot.
*
* Uses cryptographically secure random bytes to generate a private key,
* then derives the Ethereum address using the XMTP SDK.
*
* @returns Generated wallet with private key and address
*
* @example
* ```typescript
* const wallet = generateWallet();
* console.log(`Address: ${wallet.address}`);
* console.log(`Key: ${wallet.privateKey}`);
* // Address: 0x1234...abcd
* // Key: 0x...
* ```
*/
export function generateWallet(): GeneratedWallet {
// Generate 32 random bytes for private key
const privateKeyBytes = randomBytes(32);
const privateKey = `0x${privateKeyBytes.toString("hex")}` as `0x${string}`;
// Derive address using XMTP SDK
const user = createUser(privateKey);
const address = user.account.address;
return { privateKey, address };
}
/**
* Derive Ethereum address from a private key.
*
* @param privateKey - 0x-prefixed private key
* @returns Ethereum address or null if invalid
*/
export function deriveAddress(privateKey: string): string | null {
if (!privateKey || !privateKey.startsWith("0x") || privateKey.length !== 66) {
return null;
}
try {
const user = createUser(privateKey as `0x${string}`);
return user.account.address;
} catch {
return null;
}
}
/**
* Validate a wallet private key format.
*
* @param key - Potential private key
* @returns Error message or undefined if valid
*/
function validateWalletKey(key: string | undefined): string | undefined {
const trimmed = (key ?? "").trim();
if (!trimmed) return "Required";
if (!trimmed.startsWith("0x")) return "Must start with 0x";
if (trimmed.length !== 66) return "Must be 64 hex characters (+ 0x prefix)";
if (!/^0x[a-fA-F0-9]{64}$/.test(trimmed)) return "Must be valid hex characters";
// Try to derive address to verify it's a valid key
const address = deriveAddress(trimmed);
if (!address) return "Invalid private key format";
return undefined;
}
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Get XMTP config from flexible config object.
*/
function getXmtpConfig(cfg: Record<string, unknown>): XmtpConfig | undefined {
const channels = cfg.channels as Record<string, unknown> | undefined;
return channels?.xmtp as XmtpConfig | undefined;
}
/**
* Add wildcard to allowFrom list for open policy.
*/
function addWildcardAllowFrom(existing: string[] | undefined): string[] {
const current = existing ?? [];
if (current.includes("*")) return current;
return ["*", ...current];
}
/**
* Prompt user for account ID selection.
*/
async function promptAccountId(params: {
cfg: Record<string, unknown>;
prompter: Prompter;
label: string;
currentId: string;
listAccountIds: (cfg: Record<string, unknown>) => string[];
defaultAccountId: string | null;
}): Promise<string> {
const { cfg, prompter, label, currentId, listAccountIds, defaultAccountId } = params;
const existingIds = listAccountIds(cfg);
const options = [
{
value: DEFAULT_XMTP_ACCOUNT_ID,
label: `default${currentId === DEFAULT_XMTP_ACCOUNT_ID ? " (current)" : ""}`,
hint: "Main account",
},
...existingIds
.filter((id) => id !== DEFAULT_XMTP_ACCOUNT_ID)
.map((id) => ({
value: id,
label: `${id}${id === currentId ? " (current)" : ""}`,
hint: id === defaultAccountId ? "default" : undefined,
})),
{ value: "__new__", label: "Create new account", hint: "Add another XMTP identity" },
];
const selected = await prompter.select({
message: `${label} account`,
options,
initialValue: currentId,
});
if (typeof selected === "symbol") {
return currentId; // Cancelled
}
if (selected === "__new__") {
const newId = await prompter.text({
message: "New account name",
placeholder: "work, personal, bot2",
validate: (v) => {
const trimmed = (v ?? "").trim();
if (!trimmed) return "Required";
if (trimmed === DEFAULT_XMTP_ACCOUNT_ID) return "Cannot use 'default' as account name";
if (existingIds.includes(trimmed)) return "Account already exists";
if (!/^[a-z0-9_-]+$/i.test(trimmed)) return "Use only letters, numbers, dashes, underscores";
return undefined;
},
});
return typeof newId === "symbol" ? currentId : String(newId).trim();
}
return selected;
}
/**
* Display XMTP setup help note.
*/
async function noteXmtpSetupHelp(prompter: Prompter): Promise<void> {
await prompter.note(
[
"XMTP is a decentralized messaging protocol for Ethereum wallets.",
"",
"Your bot needs an Ethereum wallet (private key) to send and receive messages.",
"You can either:",
" 1) Generate a new wallet (recommended for new bots)",
" 2) Use an existing private key",
"",
"⚠️ Keep your private key secure! Anyone with access can control the bot.",
"",
"Docs: https://xmtp.org/docs",
].join("\n"),
"XMTP Setup"
);
}
/**
* Prompt for allowFrom addresses.
*/
async function promptXmtpAllowFrom(params: {
cfg: Record<string, unknown>;
prompter: Prompter;
accountId: string;
}): Promise<Record<string, unknown>> {
const { cfg, prompter, accountId } = params;
const resolved = resolveXmtpAccount({ cfg, accountId });
const existingAllowFrom = resolved?.config.allowFrom ?? [];
const entry = await prompter.text({
message: "XMTP allowFrom (Ethereum address)",
placeholder: "0x1234...5678",
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
validate: (value) => {
const raw = String(value ?? "").trim();
if (!raw) return "Required";
if (!/^0x[a-fA-F0-9]{40}$/i.test(raw)) return "Use a valid Ethereum address (0x + 40 hex)";
return undefined;
},
});
if (typeof entry === "symbol") {
return cfg; // Cancelled
}
const normalized = String(entry).trim().toLowerCase();
const merged = [
...existingAllowFrom.map((item) => String(item).trim().toLowerCase()).filter(Boolean),
normalized,
];
const unique = [...new Set(merged)];
if (accountId === DEFAULT_XMTP_ACCOUNT_ID) {
return {
...cfg,
channels: {
...(cfg.channels as Record<string, unknown>),
xmtp: {
...getXmtpConfig(cfg),
enabled: true,
dmPolicy: "allowlist",
allowFrom: unique,
},
},
};
}
const xmtpCfg = getXmtpConfig(cfg);
return {
...cfg,
channels: {
...(cfg.channels as Record<string, unknown>),
xmtp: {
...xmtpCfg,
enabled: true,
accounts: {
...xmtpCfg?.accounts,
[accountId]: {
...xmtpCfg?.accounts?.[accountId],
enabled: true,
dmPolicy: "allowlist",
allowFrom: unique,
},
},
},
},
};
}
// ============================================================================
// DM Policy Helper
// ============================================================================
/**
* Set XMTP DM policy in config.
*/
function setXmtpDmPolicy(
cfg: Record<string, unknown>,
dmPolicy: XmtpDmPolicy
): Record<string, unknown> {
const xmtpCfg = getXmtpConfig(cfg);
const allowFrom = dmPolicy === "open" ? addWildcardAllowFrom(xmtpCfg?.allowFrom) : undefined;
return {
...cfg,
channels: {
...(cfg.channels as Record<string, unknown>),
xmtp: {
...xmtpCfg,
dmPolicy,
...(allowFrom ? { allowFrom } : {}),
},
},
};
}
// ============================================================================
// Onboarding Adapter
// ============================================================================
const provider = "xmtp";
const dmPolicyAdapter: DmPolicyAdapter = {
label: "XMTP",
provider,
policyKey: "channels.xmtp.dmPolicy",
allowFromKey: "channels.xmtp.allowFrom",
getCurrent: (cfg) => {
const xmtpCfg = getXmtpConfig(cfg);
return (xmtpCfg?.dmPolicy ?? "pairing") as XmtpDmPolicy;
},
setPolicy: (cfg, policy) => setXmtpDmPolicy(cfg, policy),
};
/**
* XMTP onboarding adapter for `clawdbot onboard` wizard.
*
* Provides interactive setup for:
* - Wallet key configuration (generate new or use existing)
* - Network selection (dev vs production)
* - DM policy configuration (pairing, allowlist, open)
* - AllowFrom addresses
*/
export const xmtpOnboardingAdapter: XmtpOnboardingAdapter = {
provider,
/**
* Get current XMTP configuration status.
*/
getStatus: async ({ cfg }) => {
const configured = listXmtpAccountIds(cfg).some((accountId) =>
Boolean(resolveXmtpAccount({ cfg, accountId })?.configured)
);
// Check for env var as well
const hasEnvKey = Boolean(process.env.XMTP_WALLET_KEY?.trim());
return {
provider,
configured: configured || hasEnvKey,
statusLines: [
`XMTP: ${configured || hasEnvKey ? "configured" : "needs wallet key"}`,
],
selectionHint: configured || hasEnvKey
? "configured"
: "decentralized · wallet-to-wallet messaging",
quickstartScore: configured || hasEnvKey ? 2 : 3,
};
},
/**
* Run the XMTP configuration wizard.
*/
configure: async ({
cfg,
prompter,
accountOverrides,
shouldPromptAccountIds,
forceAllowFrom,
}) => {
const xmtpOverride = accountOverrides.xmtp?.trim();
const defaultXmtpAccountId = resolveDefaultXmtpAccountId(cfg);
let xmtpAccountId = xmtpOverride
? normalizeXmtpAccountId(xmtpOverride, cfg)
: defaultXmtpAccountId ?? DEFAULT_XMTP_ACCOUNT_ID;
// Prompt for account selection if multiple accounts or --all flag
if (shouldPromptAccountIds && !xmtpOverride) {
xmtpAccountId = await promptAccountId({
cfg,
prompter,
label: "XMTP",
currentId: xmtpAccountId,
listAccountIds: listXmtpAccountIds,
defaultAccountId: defaultXmtpAccountId,
});
}
let next = cfg;
const resolvedAccount = resolveXmtpAccount({ cfg: next, accountId: xmtpAccountId });
const accountConfigured = Boolean(resolvedAccount?.configured);
const allowEnv = xmtpAccountId === DEFAULT_XMTP_ACCOUNT_ID;
const canUseEnv = allowEnv && Boolean(process.env.XMTP_WALLET_KEY?.trim());
const hasConfigKey = Boolean(resolvedAccount?.walletKey);
let walletKey: string | null = null;
let walletAddress: string | null = null;
// Show setup help if not configured
if (!accountConfigured) {
await noteXmtpSetupHelp(prompter);
}
// Handle wallet key configuration
if (canUseEnv && !hasConfigKey) {
const keepEnv = await prompter.confirm({
message: "XMTP_WALLET_KEY detected in environment. Use env var?",
initialValue: true,
});
if (keepEnv) {
// Just enable, key comes from env
const derivedAddress = deriveAddress(process.env.XMTP_WALLET_KEY ?? "");
if (derivedAddress) {
await prompter.note(`Using wallet from environment: ${derivedAddress}`, "XMTP");
}
} else {
// Ask if they want to generate or enter
const keySource = await prompter.select({
message: "How would you like to configure the wallet?",
options: [
{ value: "generate", label: "Generate new wallet", hint: "Recommended for new bots" },
{ value: "existing", label: "Enter existing private key", hint: "Use your own key" },
],
});
if (typeof keySource !== "symbol") {
if (keySource === "generate") {
const wallet = generateWallet();
walletKey = wallet.privateKey;
walletAddress = wallet.address;
await prompter.note(
[
`Generated new wallet:`,
`Address: ${walletAddress}`,
``,
`⚠️ IMPORTANT: Save your private key securely!`,
`The key will be stored in your clawdbot.json config.`,
`Back it up if you need to recover this bot identity.`,
].join("\n"),
"New Wallet"
);
} else {
walletKey = String(
await prompter.text({
message: "Enter wallet private key",
placeholder: "0x...",
validate: validateWalletKey,
})
).trim();
if (walletKey && typeof walletKey !== "symbol") {
walletAddress = deriveAddress(walletKey);
}
}
}
}
} else if (hasConfigKey) {
// Already configured
const keep = await prompter.confirm({
message: `XMTP wallet configured (${resolvedAccount?.walletAddress ?? "unknown"}). Keep it?`,
initialValue: true,
});
if (!keep) {
const keySource = await prompter.select({
message: "How would you like to configure the wallet?",
options: [
{ value: "generate", label: "Generate new wallet", hint: "Create fresh identity" },
{ value: "existing", label: "Enter existing private key", hint: "Use your own key" },
],
});
if (typeof keySource !== "symbol") {
if (keySource === "generate") {
const wallet = generateWallet();
walletKey = wallet.privateKey;
walletAddress = wallet.address;
await prompter.note(
[
`Generated new wallet:`,
`Address: ${walletAddress}`,
``,
`⚠️ Your previous wallet will be replaced!`,
].join("\n"),
"New Wallet"
);
} else {
walletKey = String(
await prompter.text({
message: "Enter wallet private key",
placeholder: "0x...",
validate: validateWalletKey,
})
).trim();
if (walletKey && typeof walletKey !== "symbol") {
walletAddress = deriveAddress(walletKey);
}
}
}
}
} else {
// No config, no env - need to configure
const keySource = await prompter.select({
message: "How would you like to configure the wallet?",
options: [
{ value: "generate", label: "Generate new wallet", hint: "Recommended for new bots" },
{ value: "existing", label: "Enter existing private key", hint: "Use your own key" },
],
});
if (typeof keySource !== "symbol") {
if (keySource === "generate") {
const wallet = generateWallet();
walletKey = wallet.privateKey;
walletAddress = wallet.address;
await prompter.note(
[
`Generated new wallet:`,
`Address: ${walletAddress}`,
``,
`⚠️ IMPORTANT: Save your private key securely!`,
`The key will be stored in your clawdbot.json config.`,
].join("\n"),
"New Wallet"
);
} else {
walletKey = String(
await prompter.text({
message: "Enter wallet private key",
placeholder: "0x...",
validate: validateWalletKey,
})
).trim();
if (walletKey && typeof walletKey !== "symbol") {
walletAddress = deriveAddress(walletKey);
}
}
}
}
// Prompt for network selection
const currentEnv = resolvedAccount?.env ?? "dev";
const envChoice = await prompter.select<XmtpEnv>({
message: "XMTP network",
options: [
{
value: "dev" as XmtpEnv,
label: "Development",
hint: "Free, for testing (recommended to start)",
},
{
value: "production" as XmtpEnv,
label: "Production",
hint: "Real network, costs apply",
},
],
initialValue: currentEnv as XmtpEnv,
});
const env: XmtpEnv = typeof envChoice === "symbol" ? currentEnv : envChoice;
// Prompt for DM policy
const currentPolicy = resolvedAccount?.config.dmPolicy ?? "pairing";
const policyChoice = await prompter.select<XmtpDmPolicy>({
message: "DM policy (who can message the bot?)",
options: [
{
value: "pairing" as XmtpDmPolicy,
label: "Pairing",
hint: "Users request access, you approve (most secure)",
},
{
value: "allowlist" as XmtpDmPolicy,
label: "Allowlist",
hint: "Only pre-approved addresses",
},
{
value: "open" as XmtpDmPolicy,
label: "Open",
hint: "Anyone can message (not recommended)",
},
],
initialValue: currentPolicy as XmtpDmPolicy,
});
const dmPolicy: XmtpDmPolicy = typeof policyChoice === "symbol" ? currentPolicy : policyChoice;
// Build updated config
const xmtpCfg = getXmtpConfig(next);
if (xmtpAccountId === DEFAULT_XMTP_ACCOUNT_ID) {
next = {
...next,
channels: {
...(next.channels as Record<string, unknown>),
xmtp: {
...xmtpCfg,
enabled: true,
...(walletKey ? { walletKey } : {}),
env,
dmPolicy,
},
},
};
} else {
next = {
...next,
channels: {
...(next.channels as Record<string, unknown>),
xmtp: {
...xmtpCfg,
enabled: true,
accounts: {
...xmtpCfg?.accounts,
[xmtpAccountId]: {
...xmtpCfg?.accounts?.[xmtpAccountId],
enabled: true,
...(walletKey ? { walletKey } : {}),
env,
dmPolicy,
},
},
},
},
};
}
// Prompt for allowFrom only if allowlist mode (pairing doesn't need it)
if (dmPolicy === "allowlist") {
next = await promptXmtpAllowFrom({
cfg: next,
prompter,
accountId: xmtpAccountId,
});
}
// Show final config note
const finalAddress = walletAddress ?? resolvedAccount?.walletAddress;
await prompter.note(
[
`XMTP configured successfully!`,
``,
`Wallet: ${finalAddress ?? "(from env)"}`,
`Network: ${env}`,
`DM Policy: ${dmPolicy}`,
``,
`Start with: clawdbot gateway start`,
``,
env === "dev"
? "💡 Dev network is free for testing. Switch to production when ready."
: "⚠️ Production network has costs. See XMTP pricing for details.",
].join("\n"),
"XMTP Setup Complete"
);
return { cfg: next, accountId: xmtpAccountId };
},
dmPolicy: dmPolicyAdapter,
/**
* Disable XMTP channel.
*/
disable: (cfg) => {
const xmtpCfg = getXmtpConfig(cfg);
return {
...cfg,
channels: {
...(cfg.channels as Record<string, unknown>),
xmtp: {
...xmtpCfg,
enabled: false,
},
},
};
},
};

View File

@ -0,0 +1,473 @@
/**
* XMTP Channel Type Definitions
*
* Type definitions for the XMTP channel plugin, following the patterns
* established by Telegram/Discord channels in Clawdbot core.
*/
// Re-export common types from clawdbot for convenience
// (These would be imported in an integrated extension)
export type { ClawdbotRuntime } from "clawdbot/plugin-sdk";
// ============================================================================
// Base Types
// ============================================================================
/**
* XMTP network environment.
* - "dev": Development/testnet network (free, for testing)
* - "production": Production mainnet (real XMTP network)
*/
export type XmtpEnv = "dev" | "production";
/**
* DM policy for XMTP channel.
* Controls how direct messages from unknown senders are handled.
*
* - "pairing": Unknown senders get a pairing code; owner must approve
* - "allowlist": Only allow senders explicitly in allowFrom
* - "open": Allow all inbound DMs (but only allowlisted users can run commands)
*/
export type XmtpDmPolicy = "pairing" | "allowlist" | "open";
/**
* Markdown table rendering mode.
* - "off": Leave tables as-is (may not render well in XMTP clients)
* - "bullets": Convert tables to bullet lists (recommended for chat)
* - "code": Wrap tables in code blocks
*/
export type XmtpMarkdownTableMode = "off" | "bullets" | "code";
// ============================================================================
// Configuration Types
// ============================================================================
/**
* Markdown formatting configuration for XMTP messages.
*/
export type XmtpMarkdownConfig = {
/** Table rendering mode (off|bullets|code). Default: "bullets" */
tables?: XmtpMarkdownTableMode;
};
/**
* Per-action tool gating for XMTP channel.
* Controls which actions the bot can perform.
*/
export type XmtpActionConfig = {
/** Enable/disable reaction support. Default: true */
reactions?: boolean;
/** Enable/disable sending messages. Default: true */
sendMessage?: boolean;
};
/**
* Outbound retry configuration for XMTP API calls.
*/
export type XmtpRetryConfig = {
/** Max retry attempts for outbound requests. Default: 3 */
attempts?: number;
/** Minimum retry delay in ms. Default: 500 */
minDelayMs?: number;
/** Maximum retry delay cap in ms. Default: 30000 */
maxDelayMs?: number;
/** Jitter factor (0-1) applied to delays. Default: 0.1 */
jitter?: number;
};
/**
* Per-account XMTP configuration.
* Defines all settings for a single XMTP bot account.
*/
export type XmtpAccountConfig = {
/**
* Optional display name for this account.
* Used in CLI/UI lists and status displays.
*/
name?: string;
/**
* If false, do not start this XMTP account.
* Default: true (when walletKey is configured)
*/
enabled?: boolean;
/**
* Ethereum wallet private key for XMTP identity.
* Must be a 0x-prefixed 64-character hex string.
* Can also be set via XMTP_WALLET_KEY environment variable.
*/
walletKey?: string;
/**
* XMTP network environment.
* - "dev": Development network (free, for testing)
* - "production": Production mainnet
* Default: "dev"
*/
env?: XmtpEnv;
/**
* Path to XMTP database directory.
* The actual database file will be created at {dbPath}/{env}/xmtp-{inboxId}.db3
* Default: ".xmtp/db"
*/
dbPath?: string;
/**
* Optional encryption key for XMTP database.
* Must be a 0x-prefixed hex string if provided.
* Can also be set via XMTP_DB_ENCRYPTION_KEY environment variable.
*/
encryptionKey?: string;
/**
* Controls how direct messages from unknown senders are handled.
* - "pairing": Unknown senders get a pairing code; owner must approve
* - "allowlist": Only allow senders explicitly in allowFrom
* - "open": Allow all inbound DMs
* Default: "pairing"
*/
dmPolicy?: XmtpDmPolicy;
/**
* List of authorized Ethereum addresses.
* Used for allowlist/pairing policies.
* Addresses should be lowercase 0x-prefixed.
*/
allowFrom?: string[];
/**
* Markdown formatting configuration.
*/
markdown?: XmtpMarkdownConfig;
/**
* Outbound text chunk size (chars).
* Messages longer than this will be split into multiple sends.
* Default: 4000
*/
textChunkLimit?: number;
/**
* Chunking mode for long messages.
* - "length": Split by character count (default)
* - "newline": Split on every newline
*/
chunkMode?: "length" | "newline";
/**
* Per-action tool gating.
*/
actions?: XmtpActionConfig;
/**
* Retry policy for outbound XMTP API calls.
*/
retry?: XmtpRetryConfig;
/**
* Controls which user reactions trigger notifications.
* - "off": Ignore all reactions (default)
* - "own": Notify when users react to bot messages
* - "all": Notify agent of all reactions
*/
reactionNotifications?: "off" | "own" | "all";
/**
* Controls agent's reaction capability.
* - "off": Agent cannot react
* - "ack": Bot sends acknowledgment reactions (default)
* - "minimal": Agent can react sparingly
* - "extensive": Agent can react liberally
*/
reactionLevel?: "off" | "ack" | "minimal" | "extensive";
};
/**
* Top-level XMTP channel configuration.
* Supports both single-account and multi-account setups.
*
* Single account:
* ```json
* { "channels": { "xmtp": { "walletKey": "0x...", "env": "dev" } } }
* ```
*
* Multi-account:
* ```json
* { "channels": { "xmtp": { "accounts": { "main": { "walletKey": "0x..." } } } } }
* ```
*/
export type XmtpConfig = {
/** Optional per-account XMTP configuration (multi-account). */
accounts?: Record<string, XmtpAccountConfig>;
} & XmtpAccountConfig;
// ============================================================================
// Runtime Types
// ============================================================================
/**
* Resolved XMTP account state.
* This is the fully-resolved configuration for a single account,
* with defaults applied and derived values computed.
*/
export interface ResolvedXmtpAccount {
/** Account identifier (e.g., "default", "main", etc.) */
accountId: string;
/** Display name for this account */
name: string | null;
/** Whether this account is enabled */
enabled: boolean;
/** Whether wallet key is configured */
configured: boolean;
/** Wallet private key (if configured) */
walletKey: string | null;
/** Derived Ethereum address from wallet key */
walletAddress: string | null;
/** XMTP network environment */
env: XmtpEnv;
/** Path to XMTP database directory */
dbPath: string;
/** Database encryption key (if configured) */
encryptionKey: string | null;
/** Resolved config values */
config: {
/** DM handling policy */
dmPolicy: XmtpDmPolicy;
/** Authorized addresses */
allowFrom: string[];
};
}
/**
* Runtime state for an XMTP account.
* Tracks the operational status of the XMTP gateway.
* Extends Record<string, unknown> for clawdbot SDK compatibility.
*/
export interface XmtpRuntimeState extends Record<string, unknown> {
/** Account identifier */
accountId: string;
/** Whether the gateway is currently running */
running: boolean;
/** Timestamp of last gateway start */
lastStartAt: number | null;
/** Timestamp of last gateway stop */
lastStopAt: number | null;
/** Last error message (if any) */
lastError: string | null;
/** Timestamp of last inbound message received */
lastInboundAt?: number | null;
/** Timestamp of last outbound message sent */
lastOutboundAt?: number | null;
}
/**
* Status snapshot for an XMTP account.
* Used for CLI status displays and health checks.
* Extends Record<string, unknown> for clawdbot SDK compatibility.
*/
export interface XmtpAccountSnapshot extends Record<string, unknown> {
/** Account identifier */
accountId: string;
/** Display name */
name: string | null;
/** Whether account is enabled in config */
enabled: boolean;
/** Whether wallet key is configured */
configured: boolean;
/** Ethereum wallet address */
walletAddress: string | null;
/** Network environment */
env: XmtpEnv;
/** Whether gateway is currently running */
running: boolean;
/** Timestamp of last gateway start */
lastStartAt: number | null;
/** Timestamp of last gateway stop */
lastStopAt: number | null;
/** Last error message */
lastError: string | null;
/** Timestamp of last inbound message */
lastInboundAt?: number | null;
/** Timestamp of last outbound message */
lastOutboundAt?: number | null;
}
/**
* Channel-level status summary for XMTP.
* Aggregated view across all accounts.
* Extends Record<string, unknown> for clawdbot SDK compatibility.
*/
export interface XmtpChannelSummary extends Record<string, unknown> {
/** Whether any account is configured */
configured: boolean;
/** Primary wallet address */
walletAddress: string | null;
/** Network environment */
env: XmtpEnv;
/** Whether any account is running */
running: boolean;
/** Timestamp of most recent start */
lastStartAt: number | null;
/** Timestamp of most recent stop */
lastStopAt: number | null;
/** Most recent error */
lastError: string | null;
}
// ============================================================================
// Message Types
// ============================================================================
/**
* Inbound message context for XMTP.
* Contains all information about an incoming message.
*/
export interface XmtpInboundContext {
/** Message body text */
Body: string;
/** Raw message body (unprocessed) */
RawBody: string;
/** Sender address with xmtp: prefix */
From: string;
/** Conversation ID with xmtp: prefix */
To: string;
/** Session key for conversation tracking */
SessionKey: string;
/** Account ID handling this message */
AccountId: string;
/** Chat type: "direct" or "group" */
ChatType: "direct" | "group";
/** Human-readable label for conversation */
ConversationLabel: string;
/** Sender display name (usually address) */
SenderName: string;
/** Sender identifier */
SenderId: string;
/** Provider name */
Provider: "xmtp";
/** Surface/platform */
Surface: "xmtp";
/** Unique message ID */
MessageSid: string;
/** Conversation ID */
ConversationId: string;
/** Sender's XMTP inbox ID */
SenderInboxId: string;
/** Message timestamp */
Timestamp: number;
/** Whether sender can execute commands */
CommandAuthorized: boolean;
/** Original channel name */
OriginatingChannel: "xmtp";
/** Original conversation target */
OriginatingTo: string;
/** Reply-to message ID (if this is a reply) */
ReplyToId?: string;
}
/**
* Authorization check result for an XMTP sender.
*/
export interface XmtpAuthorizationResult {
/** Whether the sender is allowed to message */
allowed: boolean;
/** Whether the sender can execute commands */
commandAuthorized: boolean;
/** Reason for denial (if not allowed) */
reason?: "needs_pairing" | "not_in_allowlist" | "unknown_policy";
}
// ============================================================================
// Directory Types
// ============================================================================
/**
* Directory entry for XMTP peer (user).
*/
export interface XmtpPeerEntry {
kind: "user";
/** Ethereum address or inbox ID */
id: string;
/** Display name (usually same as address) */
name: string;
}
/**
* Directory entry for XMTP group.
*/
export interface XmtpGroupEntry {
kind: "group";
/** Group conversation ID */
id: string;
/** Group name */
name: string;
}
// ============================================================================
// Action Types
// ============================================================================
/**
* Result from an XMTP action (e.g., react).
*/
export interface XmtpActionResult {
content: Array<{ type: "text"; text: string }>;
details: Record<string, unknown>;
}
/**
* Parameters for the react action.
* All fields are optional because params come from untyped input.
*/
export interface XmtpReactParams {
/** Conversation ID (or "to" as alias) */
conversationId?: string;
/** Alias for conversationId */
to?: string;
/** Message ID to react to */
messageId?: string;
/** Emoji to react with. Default: "👍" */
emoji?: string;
/** Whether to remove the reaction. Default: false */
remove?: boolean;
/** Sender inbox ID (required for group messages) */
senderInboxId?: string;
}
// ============================================================================
// Plugin Types
// ============================================================================
/**
* Status issue from XMTP channel.
*/
export interface XmtpStatusIssue {
channel: "xmtp";
accountId: string;
kind: "runtime";
message: string;
}
/**
* XMTP plugin metadata.
*/
export interface XmtpPluginMeta {
id: "xmtp";
label: "XMTP";
selectionLabel: "XMTP (Decentralized Messaging)";
docsPath: "/channels/xmtp";
docsLabel: "xmtp";
blurb: "Decentralized messaging via XMTP protocol";
order: number;
}
/**
* XMTP plugin capabilities.
*/
export interface XmtpPluginCapabilities {
chatTypes: Array<"direct" | "group">;
reactions: boolean;
media: boolean;
}