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:
parent
047070a5cf
commit
893d1e60e9
1188
extensions/xmtp/src/channel.ts
Normal file
1188
extensions/xmtp/src/channel.ts
Normal file
File diff suppressed because it is too large
Load Diff
752
extensions/xmtp/src/onboarding.ts
Normal file
752
extensions/xmtp/src/onboarding.ts
Normal 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
473
extensions/xmtp/src/types.xmtp.ts
Normal file
473
extensions/xmtp/src/types.xmtp.ts
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user