chore: migrate to oxlint and oxfmt

Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
Peter Steinberger
2026-01-14 14:31:43 +00:00
parent 912ebffc63
commit c379191f80
1480 changed files with 28608 additions and 43547 deletions

View File

@@ -1,9 +1,6 @@
import { createActionGate } from "../../../agents/tools/common.js";
import { listEnabledDiscordAccounts } from "../../../discord/accounts.js";
import type {
ChannelMessageActionAdapter,
ChannelMessageActionName,
} from "../types.js";
import type { ChannelMessageActionAdapter, ChannelMessageActionName } from "../types.js";
import { handleDiscordMessageAction } from "./discord/handle-action.js";
export const discordMessageActions: ChannelMessageActionAdapter = {
@@ -78,8 +75,7 @@ export const discordMessageActions: ChannelMessageActionAdapter = {
return to ? { to } : null;
}
if (action === "threadReply") {
const channelId =
typeof args.channelId === "string" ? args.channelId.trim() : "";
const channelId = typeof args.channelId === "string" ? args.channelId.trim() : "";
return channelId ? { to: `channel:${channelId}` } : null;
}
return null;

View File

@@ -12,9 +12,7 @@ type Ctx = Pick<ChannelMessageActionContext, "action" | "params" | "cfg">;
export async function tryHandleDiscordMessageActionGuildAdmin(params: {
ctx: Ctx;
resolveChannelId: () => string;
readParentIdParam: (
params: Record<string, unknown>,
) => string | null | undefined;
readParentIdParam: (params: Record<string, unknown>) => string | null | undefined;
}): Promise<AgentToolResult<unknown> | undefined> {
const { ctx, resolveChannelId, readParentIdParam } = params;
const { action, params: actionParams, cfg } = ctx;
@@ -24,10 +22,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
const guildId = readStringParam(actionParams, "guildId", {
required: true,
});
return await handleDiscordAction(
{ action: "memberInfo", guildId, userId },
cfg,
);
return await handleDiscordAction({ action: "memberInfo", guildId, userId }, cfg);
}
if (action === "role-info") {
@@ -125,8 +120,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
const position = readNumberParam(actionParams, "position", {
integer: true,
});
const nsfw =
typeof actionParams.nsfw === "boolean" ? actionParams.nsfw : undefined;
const nsfw = typeof actionParams.nsfw === "boolean" ? actionParams.nsfw : undefined;
return await handleDiscordAction(
{
action: "channelCreate",
@@ -152,8 +146,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
integer: true,
});
const parentId = readParentIdParam(actionParams);
const nsfw =
typeof actionParams.nsfw === "boolean" ? actionParams.nsfw : undefined;
const nsfw = typeof actionParams.nsfw === "boolean" ? actionParams.nsfw : undefined;
const rateLimitPerUser = readNumberParam(actionParams, "rateLimitPerUser", {
integer: true,
});
@@ -176,10 +169,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
const channelId = readStringParam(actionParams, "channelId", {
required: true,
});
return await handleDiscordAction(
{ action: "channelDelete", channelId },
cfg,
);
return await handleDiscordAction({ action: "channelDelete", channelId }, cfg);
}
if (action === "channel-move") {
@@ -247,10 +237,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
const categoryId = readStringParam(actionParams, "categoryId", {
required: true,
});
return await handleDiscordAction(
{ action: "categoryDelete", categoryId },
cfg,
);
return await handleDiscordAction({ action: "categoryDelete", categoryId }, cfg);
}
if (action === "voice-status") {
@@ -258,10 +245,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
required: true,
});
const userId = readStringParam(actionParams, "userId", { required: true });
return await handleDiscordAction(
{ action: "voiceStatus", guildId, userId },
cfg,
);
return await handleDiscordAction({ action: "voiceStatus", guildId, userId }, cfg);
}
if (action === "event-list") {
@@ -335,9 +319,7 @@ export async function tryHandleDiscordMessageActionGuildAdmin(params: {
});
const channelId = readStringParam(actionParams, "channelId");
const includeArchived =
typeof actionParams.includeArchived === "boolean"
? actionParams.includeArchived
: undefined;
typeof actionParams.includeArchived === "boolean" ? actionParams.includeArchived : undefined;
const before = readStringParam(actionParams, "before");
const limit = readNumberParam(actionParams, "limit", { integer: true });
return await handleDiscordAction(

View File

@@ -10,9 +10,7 @@ import { tryHandleDiscordMessageActionGuildAdmin } from "./handle-action.guild-a
const providerId = "discord";
function readParentIdParam(
params: Record<string, unknown>,
): string | null | undefined {
function readParentIdParam(params: Record<string, unknown>): string | null | undefined {
if (params.clearParent === true) return null;
if (params.parentId === null) return null;
return readStringParam(params, "parentId");
@@ -24,8 +22,7 @@ export async function handleDiscordMessageAction(
const { action, params, cfg } = ctx;
const resolveChannelId = () =>
readStringParam(params, "channelId") ??
readStringParam(params, "to", { required: true });
readStringParam(params, "channelId") ?? readStringParam(params, "to", { required: true });
if (action === "send") {
const to = readStringParam(params, "to", { required: true });
@@ -52,10 +49,8 @@ export async function handleDiscordMessageAction(
const question = readStringParam(params, "pollQuestion", {
required: true,
});
const answers =
readStringArrayParam(params, "pollOption", { required: true }) ?? [];
const allowMultiselect =
typeof params.pollMulti === "boolean" ? params.pollMulti : undefined;
const answers = readStringArrayParam(params, "pollOption", { required: true }) ?? [];
const allowMultiselect = typeof params.pollMulti === "boolean" ? params.pollMulti : undefined;
const durationHours = readNumberParam(params, "pollDurationHours", {
integer: true,
});
@@ -76,8 +71,7 @@ export async function handleDiscordMessageAction(
if (action === "react") {
const messageId = readStringParam(params, "messageId", { required: true });
const emoji = readStringParam(params, "emoji", { allowEmpty: true });
const remove =
typeof params.remove === "boolean" ? params.remove : undefined;
const remove = typeof params.remove === "boolean" ? params.remove : undefined;
return await handleDiscordAction(
{
action: "react",
@@ -138,17 +132,10 @@ export async function handleDiscordMessageAction(
if (action === "pin" || action === "unpin" || action === "list-pins") {
const messageId =
action === "list-pins"
? undefined
: readStringParam(params, "messageId", { required: true });
action === "list-pins" ? undefined : readStringParam(params, "messageId", { required: true });
return await handleDiscordAction(
{
action:
action === "pin"
? "pinMessage"
: action === "unpin"
? "unpinMessage"
: "listPins",
action: action === "pin" ? "pinMessage" : action === "unpin" ? "unpinMessage" : "listPins",
channelId: resolveChannelId(),
messageId,
},
@@ -157,10 +144,7 @@ export async function handleDiscordMessageAction(
}
if (action === "permissions") {
return await handleDiscordAction(
{ action: "permissions", channelId: resolveChannelId() },
cfg,
);
return await handleDiscordAction({ action: "permissions", channelId: resolveChannelId() }, cfg);
}
if (action === "thread-create") {
@@ -205,7 +189,5 @@ export async function handleDiscordMessageAction(
});
if (adminResult !== undefined) return adminResult;
throw new Error(
`Action ${String(action)} is not supported for provider ${providerId}.`,
);
throw new Error(`Action ${String(action)} is not supported for provider ${providerId}.`);
}

View File

@@ -1,14 +1,8 @@
import {
createActionGate,
readStringParam,
} from "../../../agents/tools/common.js";
import { createActionGate, readStringParam } from "../../../agents/tools/common.js";
import { handleTelegramAction } from "../../../agents/tools/telegram-actions.js";
import type { ClawdbotConfig } from "../../../config/config.js";
import { listEnabledTelegramAccounts } from "../../../telegram/accounts.js";
import type {
ChannelMessageActionAdapter,
ChannelMessageActionName,
} from "../types.js";
import type { ChannelMessageActionAdapter, ChannelMessageActionName } from "../types.js";
const providerId = "telegram";
@@ -49,8 +43,7 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
if (action !== "sendMessage") return null;
const to = typeof args.to === "string" ? args.to : undefined;
if (!to) return null;
const accountId =
typeof args.accountId === "string" ? args.accountId.trim() : undefined;
const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined;
return { to, accountId };
},
handleAction: async ({ action, params, cfg, accountId }) => {
@@ -84,14 +77,12 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
required: true,
});
const emoji = readStringParam(params, "emoji", { allowEmpty: true });
const remove =
typeof params.remove === "boolean" ? params.remove : undefined;
const remove = typeof params.remove === "boolean" ? params.remove : undefined;
return await handleTelegramAction(
{
action: "react",
chatId:
readStringParam(params, "chatId") ??
readStringParam(params, "to", { required: true }),
readStringParam(params, "chatId") ?? readStringParam(params, "to", { required: true }),
messageId,
emoji,
remove,
@@ -101,8 +92,6 @@ export const telegramMessageActions: ChannelMessageActionAdapter = {
);
}
throw new Error(
`Action ${action} is not supported for provider ${providerId}.`,
);
throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
},
};

View File

@@ -5,8 +5,7 @@ export function createWhatsAppLoginTool(): ChannelAgentTool {
return {
label: "WhatsApp Login",
name: "whatsapp_login",
description:
"Generate a WhatsApp QR code for linking, or wait for the scan to complete.",
description: "Generate a WhatsApp QR code for linking, or wait for the scan to complete.",
// NOTE: Using Type.Unsafe for action enum instead of Type.Union([Type.Literal(...)]
// because Claude API on Vertex AI rejects nested anyOf schemas as invalid JSON Schema.
parameters: Type.Object({
@@ -18,9 +17,7 @@ export function createWhatsAppLoginTool(): ChannelAgentTool {
force: Type.Optional(Type.Boolean()),
}),
execute: async (_toolCallId, args) => {
const { startWebLoginWithQr, waitForWebLogin } = await import(
"../../../web/login-qr.js"
);
const { startWebLoginWithQr, waitForWebLogin } = await import("../../../web/login-qr.js");
const action = (args as { action?: string })?.action ?? "start";
if (action === "wait") {
const result = await waitForWebLogin({

View File

@@ -17,11 +17,7 @@ export function setAccountEnabledInConfigSection(params: {
const channels = params.cfg.channels as Record<string, unknown> | undefined;
const base = channels?.[params.sectionKey] as ChannelSection | undefined;
const hasAccounts = Boolean(base?.accounts);
if (
params.allowTopLevel &&
accountKey === DEFAULT_ACCOUNT_ID &&
!hasAccounts
) {
if (params.allowTopLevel && accountKey === DEFAULT_ACCOUNT_ID && !hasAccounts) {
return {
...params.cfg,
channels: {
@@ -34,10 +30,7 @@ export function setAccountEnabledInConfigSection(params: {
} as ClawdbotConfig;
}
const baseAccounts = (base?.accounts ?? {}) as Record<
string,
Record<string, unknown>
>;
const baseAccounts = (base?.accounts ?? {}) as Record<string, Record<string, unknown>>;
const existing = baseAccounts[accountKey] ?? {};
return {
...params.cfg,
@@ -69,9 +62,7 @@ export function deleteAccountFromConfigSection(params: {
if (!base) return params.cfg;
const baseAccounts =
base.accounts && typeof base.accounts === "object"
? { ...base.accounts }
: undefined;
base.accounts && typeof base.accounts === "object" ? { ...base.accounts } : undefined;
if (accountKey !== DEFAULT_ACCOUNT_ID) {
const accounts = baseAccounts ? { ...baseAccounts } : {};

View File

@@ -11,10 +11,7 @@ import {
import { probeDiscord } from "../../discord/probe.js";
import { sendMessageDiscord, sendPollDiscord } from "../../discord/send.js";
import { shouldLogVerbose } from "../../globals.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../../routing/session-key.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import { getChatChannelMeta } from "../registry.js";
import { discordMessageActions } from "./actions/discord.js";
import {
@@ -62,8 +59,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
reload: { configPrefixes: ["channels.discord"] },
config: {
listAccountIds: (cfg) => listDiscordAccountIds(cfg),
resolveAccount: (cfg, accountId) =>
resolveDiscordAccount({ cfg, accountId }),
resolveAccount: (cfg, accountId) => resolveDiscordAccount({ cfg, accountId }),
defaultAccountId: (cfg) => resolveDefaultDiscordAccountId(cfg),
setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({
@@ -89,9 +85,9 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
tokenSource: account.tokenSource,
}),
resolveAllowFrom: ({ cfg, accountId }) =>
(
resolveDiscordAccount({ cfg, accountId }).config.dm?.allowFrom ?? []
).map((entry) => String(entry)),
(resolveDiscordAccount({ cfg, accountId }).config.dm?.allowFrom ?? []).map((entry) =>
String(entry),
),
formatAllowFrom: ({ allowFrom }) =>
allowFrom
.map((entry) => String(entry).trim())
@@ -100,11 +96,8 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
},
security: {
resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId =
accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
const useAccountPath = Boolean(
cfg.channels?.discord?.accounts?.[resolvedAccountId],
);
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
const useAccountPath = Boolean(cfg.channels?.discord?.accounts?.[resolvedAccountId]);
const allowFromPath = useAccountPath
? `channels.discord.accounts.${resolvedAccountId}.dm.`
: "channels.discord.dm.";
@@ -113,16 +106,14 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
allowFrom: account.config.dm?.allowFrom ?? [],
allowFromPath,
approveHint: formatPairingApproveHint("discord"),
normalizeEntry: (raw) =>
raw.replace(/^(discord|user):/i, "").replace(/^<@!?(\d+)>$/, "$1"),
normalizeEntry: (raw) => raw.replace(/^(discord|user):/i, "").replace(/^<@!?(\d+)>$/, "$1"),
};
},
collectWarnings: ({ account }) => {
const groupPolicy = account.config.groupPolicy ?? "allowlist";
if (groupPolicy !== "open") return [];
const channelAllowlistConfigured =
Boolean(account.config.guilds) &&
Object.keys(account.config.guilds ?? {}).length > 0;
Boolean(account.config.guilds) && Object.keys(account.config.guilds ?? {}).length > 0;
if (channelAllowlistConfigured) {
return [
`- Discord guilds: groupPolicy="open" allows any channel not explicitly denied to trigger (mention-gated). Set channels.discord.groupPolicy="allowlist" and configure channels.discord.guilds.<id>.channels.`,
@@ -140,8 +131,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
stripPatterns: () => ["<@!?\\d+>"],
},
threading: {
resolveReplyToMode: ({ cfg }) =>
cfg.channels?.discord?.replyToMode ?? "off",
resolveReplyToMode: ({ cfg }) => cfg.channels?.discord?.replyToMode ?? "off",
},
messaging: {
normalizeTarget: normalizeDiscordMessagingTarget,
@@ -187,11 +177,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
discord: {
...next.channels?.discord,
enabled: true,
...(input.useEnv
? {}
: input.token
? { token: input.token }
: {}),
...(input.useEnv ? {} : input.token ? { token: input.token } : {}),
},
},
};
@@ -226,9 +212,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
if (!trimmed) {
return {
ok: false,
error: new Error(
"Delivering to Discord requires --to <channelId|user:ID|channel:ID>",
),
error: new Error("Delivering to Discord requires --to <channelId|user:ID|channel:ID>"),
};
}
return { ok: true, to: trimmed };
@@ -304,9 +288,7 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
},
buildAccountSnapshot: ({ account, runtime, probe, audit }) => {
const configured = Boolean(account.token?.trim());
const app =
runtime?.application ??
(probe as { application?: unknown })?.application;
const app = runtime?.application ?? (probe as { application?: unknown })?.application;
const bot = runtime?.bot ?? (probe as { bot?: unknown })?.bot;
return {
accountId: account.accountId,
@@ -355,14 +337,10 @@ export const discordPlugin: ChannelPlugin<ResolvedDiscordAccount> = {
}
} catch (err) {
if (shouldLogVerbose()) {
ctx.log?.debug?.(
`[${account.accountId}] bot probe failed: ${String(err)}`,
);
ctx.log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`);
}
}
ctx.log?.info(
`[${account.accountId}] starting provider${discordBotLabel}`,
);
ctx.log?.info(`[${account.accountId}] starting provider${discordBotLabel}`);
// Lazy import: the monitor pulls the reply pipeline; avoid ESM init cycles.
const { monitorDiscordProvider } = await import("../../discord/index.js");
return monitorDiscordProvider({

View File

@@ -57,8 +57,7 @@ function resolveTelegramRequireMention(params: {
if (!chatId) return undefined;
const groupConfig = cfg.channels?.telegram?.groups?.[chatId];
const groupDefault = cfg.channels?.telegram?.groups?.["*"];
const topicConfig =
topicId && groupConfig?.topics ? groupConfig.topics[topicId] : undefined;
const topicConfig = topicId && groupConfig?.topics ? groupConfig.topics[topicId] : undefined;
const defaultTopicConfig =
topicId && groupDefault?.topics ? groupDefault.topics[topicId] : undefined;
if (typeof topicConfig?.requireMention === "boolean") {
@@ -76,10 +75,7 @@ function resolveTelegramRequireMention(params: {
return undefined;
}
function resolveDiscordGuildEntry(
guilds: DiscordConfig["guilds"],
groupSpace?: string | null,
) {
function resolveDiscordGuildEntry(guilds: DiscordConfig["guilds"], groupSpace?: string | null) {
if (!guilds || Object.keys(guilds).length === 0) return null;
const space = groupSpace?.trim() ?? "";
if (space && guilds[space]) return guilds[space];
@@ -112,9 +108,7 @@ export function resolveTelegramGroupRequireMention(
});
}
export function resolveWhatsAppGroupRequireMention(
params: GroupMentionParams,
): boolean {
export function resolveWhatsAppGroupRequireMention(params: GroupMentionParams): boolean {
return resolveChannelGroupRequireMention({
cfg: params.cfg,
channel: "whatsapp",
@@ -123,9 +117,7 @@ export function resolveWhatsAppGroupRequireMention(
});
}
export function resolveIMessageGroupRequireMention(
params: GroupMentionParams,
): boolean {
export function resolveIMessageGroupRequireMention(params: GroupMentionParams): boolean {
return resolveChannelGroupRequireMention({
cfg: params.cfg,
channel: "imessage",
@@ -134,9 +126,7 @@ export function resolveIMessageGroupRequireMention(
});
}
export function resolveDiscordGroupRequireMention(
params: GroupMentionParams,
): boolean {
export function resolveDiscordGroupRequireMention(params: GroupMentionParams): boolean {
const guildEntry = resolveDiscordGuildEntry(
params.cfg.channels?.discord?.guilds,
params.groupSpace,
@@ -149,9 +139,7 @@ export function resolveDiscordGroupRequireMention(
(channelSlug
? (channelEntries[channelSlug] ?? channelEntries[`#${channelSlug}`])
: undefined) ??
(params.groupRoom
? channelEntries[normalizeDiscordSlug(params.groupRoom)]
: undefined);
(params.groupRoom ? channelEntries[normalizeDiscordSlug(params.groupRoom)] : undefined);
if (entry && typeof entry.requireMention === "boolean") {
return entry.requireMention;
}
@@ -162,9 +150,7 @@ export function resolveDiscordGroupRequireMention(
return true;
}
export function resolveSlackGroupRequireMention(
params: GroupMentionParams,
): boolean {
export function resolveSlackGroupRequireMention(params: GroupMentionParams): boolean {
const account = resolveSlackAccount({
cfg: params.cfg,
accountId: params.accountId,

View File

@@ -8,13 +8,8 @@ export function resolveChannelDefaultAccountId<ResolvedAccount>(params: {
cfg: ClawdbotConfig;
accountIds?: string[];
}): string {
const accountIds =
params.accountIds ?? params.plugin.config.listAccountIds(params.cfg);
return (
params.plugin.config.defaultAccountId?.(params.cfg) ??
accountIds[0] ??
DEFAULT_ACCOUNT_ID
);
const accountIds = params.accountIds ?? params.plugin.config.listAccountIds(params.cfg);
return params.plugin.config.defaultAccountId?.(params.cfg) ?? accountIds[0] ?? DEFAULT_ACCOUNT_ID;
}
export function formatPairingApproveHint(channelId: string): string {

View File

@@ -7,10 +7,7 @@ import {
} from "../../imessage/accounts.js";
import { probeIMessage } from "../../imessage/probe.js";
import { sendMessageIMessage } from "../../imessage/send.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../../routing/session-key.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import { getChatChannelMeta } from "../registry.js";
import {
deleteAccountFromConfigSection,
@@ -49,8 +46,7 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
reload: { configPrefixes: ["channels.imessage"] },
config: {
listAccountIds: (cfg) => listIMessageAccountIds(cfg),
resolveAccount: (cfg, accountId) =>
resolveIMessageAccount({ cfg, accountId }),
resolveAccount: (cfg, accountId) => resolveIMessageAccount({ cfg, accountId }),
defaultAccountId: (cfg) => resolveDefaultIMessageAccountId(cfg),
setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({
@@ -75,19 +71,16 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
configured: account.configured,
}),
resolveAllowFrom: ({ cfg, accountId }) =>
(resolveIMessageAccount({ cfg, accountId }).config.allowFrom ?? []).map(
(entry) => String(entry),
(resolveIMessageAccount({ cfg, accountId }).config.allowFrom ?? []).map((entry) =>
String(entry),
),
formatAllowFrom: ({ allowFrom }) =>
allowFrom.map((entry) => String(entry).trim()).filter(Boolean),
},
security: {
resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId =
accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
const useAccountPath = Boolean(
cfg.channels?.imessage?.accounts?.[resolvedAccountId],
);
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
const useAccountPath = Boolean(cfg.channels?.imessage?.accounts?.[resolvedAccountId]);
const basePath = useAccountPath
? `channels.imessage.accounts.${resolvedAccountId}.`
: "channels.imessage.";
@@ -181,9 +174,7 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
if (!trimmed) {
return {
ok: false,
error: new Error(
"Delivering to iMessage requires --to <handle|chat_id:ID>",
),
error: new Error("Delivering to iMessage requires --to <handle|chat_id:ID>"),
};
}
return { ok: true, to: trimmed };
@@ -232,8 +223,7 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
},
collectStatusIssues: (accounts) =>
accounts.flatMap((account) => {
const lastError =
typeof account.lastError === "string" ? account.lastError.trim() : "";
const lastError = typeof account.lastError === "string" ? account.lastError.trim() : "";
if (!lastError) return [];
return [
{
@@ -287,9 +277,7 @@ export const imessagePlugin: ChannelPlugin<ResolvedIMessageAccount> = {
`[${account.accountId}] starting provider (${cliPath}${dbPath ? ` db=${dbPath}` : ""})`,
);
// Lazy import: the monitor pulls the reply pipeline; avoid ESM init cycles.
const { monitorIMessageProvider } = await import(
"../../imessage/index.js"
);
const { monitorIMessageProvider } = await import("../../imessage/index.js");
return monitorIMessageProvider({
accountId: account.accountId,
config: ctx.cfg,

View File

@@ -1,8 +1,4 @@
import {
CHAT_CHANNEL_ORDER,
type ChatChannelId,
normalizeChatChannelId,
} from "../registry.js";
import { CHAT_CHANNEL_ORDER, type ChatChannelId, normalizeChatChannelId } from "../registry.js";
import { discordPlugin } from "./discord.js";
import { imessagePlugin } from "./imessage.js";
import { msteamsPlugin } from "./msteams.js";

View File

@@ -18,9 +18,7 @@ const LOADERS: Record<ChannelId, PluginLoader> = {
const cache = new Map<ChannelId, ChannelPlugin>();
export async function loadChannelPlugin(
id: ChannelId,
): Promise<ChannelPlugin | undefined> {
export async function loadChannelPlugin(id: ChannelId): Promise<ChannelPlugin | undefined> {
const cached = cache.get(id);
if (cached) return cached;
const loader = LOADERS[id];

View File

@@ -7,10 +7,7 @@ export function resolveChannelMediaMaxBytes(params: {
cfg: ClawdbotConfig;
// Channel-specific config lives under different keys; keep this helper generic
// so shared plugin helpers don't need channel-id branching.
resolveChannelLimitMb: (params: {
cfg: ClawdbotConfig;
accountId: string;
}) => number | undefined;
resolveChannelLimitMb: (params: { cfg: ClawdbotConfig; accountId: string }) => number | undefined;
accountId?: string | null;
}): number | undefined {
const accountId = normalizeAccountId(params.accountId);

View File

@@ -39,5 +39,4 @@ export const CHANNEL_MESSAGE_ACTION_NAMES = [
"ban",
] as const;
export type ChannelMessageActionName =
(typeof CHANNEL_MESSAGE_ACTION_NAMES)[number];
export type ChannelMessageActionName = (typeof CHANNEL_MESSAGE_ACTION_NAMES)[number];

View File

@@ -2,14 +2,9 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import type { ClawdbotConfig } from "../../config/config.js";
import { getChannelPlugin, listChannelPlugins } from "./index.js";
import type {
ChannelMessageActionContext,
ChannelMessageActionName,
} from "./types.js";
import type { ChannelMessageActionContext, ChannelMessageActionName } from "./types.js";
export function listChannelMessageActions(
cfg: ClawdbotConfig,
): ChannelMessageActionName[] {
export function listChannelMessageActions(cfg: ClawdbotConfig): ChannelMessageActionName[] {
const actions = new Set<ChannelMessageActionName>(["send"]);
for (const plugin of listChannelPlugins()) {
const list = plugin.actions?.listActions?.({ cfg });
@@ -31,10 +26,7 @@ export async function dispatchChannelMessageAction(
): Promise<AgentToolResult<unknown> | null> {
const plugin = getChannelPlugin(ctx.channel);
if (!plugin?.actions?.handleAction) return null;
if (
plugin.actions.supportsAction &&
!plugin.actions.supportsAction({ action: ctx.action })
) {
if (plugin.actions.supportsAction && !plugin.actions.supportsAction({ action: ctx.action })) {
return null;
}
return await plugin.actions.handleAction(ctx);

View File

@@ -76,8 +76,7 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
}
return next;
},
isConfigured: (_account, cfg) =>
Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)),
isConfigured: (_account, cfg) => Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams)),
describeAccount: (account) => ({
accountId: account.accountId,
enabled: account.enabled,
@@ -139,17 +138,14 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount> = {
return { ok: true, to: trimmed };
},
sendText: async ({ cfg, to, text, deps }) => {
const send =
deps?.sendMSTeams ??
((to, text) => sendMessageMSTeams({ cfg, to, text }));
const send = deps?.sendMSTeams ?? ((to, text) => sendMessageMSTeams({ cfg, to, text }));
const result = await send(to, text);
return { channel: "msteams", ...result };
},
sendMedia: async ({ cfg, to, text, mediaUrl, deps }) => {
const send =
deps?.sendMSTeams ??
((to, text, opts) =>
sendMessageMSTeams({ cfg, to, text, mediaUrl: opts?.mediaUrl }));
((to, text, opts) => sendMessageMSTeams({ cfg, to, text, mediaUrl: opts?.mediaUrl }));
const result = await send(to, text, { mediaUrl });
return { channel: "msteams", ...result };
},

View File

@@ -32,9 +32,7 @@ export function normalizeSlackMessagingTarget(raw: string): string | undefined {
return `channel:${trimmed}`.toLowerCase();
}
export function normalizeDiscordMessagingTarget(
raw: string,
): string | undefined {
export function normalizeDiscordMessagingTarget(raw: string): string | undefined {
const trimmed = raw.trim();
if (!trimmed) return undefined;
const mentionMatch = trimmed.match(/^<@!?(\d+)>$/);
@@ -62,9 +60,7 @@ export function normalizeDiscordMessagingTarget(
return `channel:${trimmed}`.toLowerCase();
}
export function normalizeTelegramMessagingTarget(
raw: string,
): string | undefined {
export function normalizeTelegramMessagingTarget(raw: string): string | undefined {
const trimmed = raw.trim();
if (!trimmed) return undefined;
let normalized = trimmed;
@@ -84,9 +80,7 @@ export function normalizeTelegramMessagingTarget(
return `telegram:${normalized}`.toLowerCase();
}
export function normalizeSignalMessagingTarget(
raw: string,
): string | undefined {
export function normalizeSignalMessagingTarget(raw: string): string | undefined {
const trimmed = raw.trim();
if (!trimmed) return undefined;
let normalized = trimmed;
@@ -110,9 +104,7 @@ export function normalizeSignalMessagingTarget(
return normalized.toLowerCase();
}
export function normalizeWhatsAppMessagingTarget(
raw: string,
): string | undefined {
export function normalizeWhatsAppMessagingTarget(raw: string): string | undefined {
const trimmed = raw.trim();
if (!trimmed) return undefined;
return normalizeWhatsAppTarget(trimmed) ?? undefined;

View File

@@ -30,9 +30,7 @@ export type PromptAccountIdParams = {
defaultAccountId: string;
};
export type PromptAccountId = (
params: PromptAccountIdParams,
) => Promise<string>;
export type PromptAccountId = (params: PromptAccountIdParams) => Promise<string>;
export type ChannelOnboardingStatus = {
channel: ChatChannelId;
@@ -74,16 +72,9 @@ export type ChannelOnboardingDmPolicy = {
export type ChannelOnboardingAdapter = {
channel: ChatChannelId;
getStatus: (
ctx: ChannelOnboardingStatusContext,
) => Promise<ChannelOnboardingStatus>;
configure: (
ctx: ChannelOnboardingConfigureContext,
) => Promise<ChannelOnboardingResult>;
getStatus: (ctx: ChannelOnboardingStatusContext) => Promise<ChannelOnboardingStatus>;
configure: (ctx: ChannelOnboardingConfigureContext) => Promise<ChannelOnboardingResult>;
dmPolicy?: ChannelOnboardingDmPolicy;
onAccountRecorded?: (
accountId: string,
options?: SetupChannelsOptions,
) => void;
onAccountRecorded?: (accountId: string, options?: SetupChannelsOptions) => void;
disable?: (cfg: ClawdbotConfig) => ClawdbotConfig;
};

View File

@@ -5,25 +5,17 @@ import {
resolveDefaultDiscordAccountId,
resolveDiscordAccount,
} from "../../../discord/accounts.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../../../routing/session-key.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
import { formatDocsLink } from "../../../terminal/links.js";
import type { WizardPrompter } from "../../../wizard/prompts.js";
import type {
ChannelOnboardingAdapter,
ChannelOnboardingDmPolicy,
} from "../onboarding-types.js";
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
const channel = "discord" as const;
function setDiscordDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
const allowFrom =
dmPolicy === "open"
? addWildcardAllowFrom(cfg.channels?.discord?.dm?.allowFrom)
: undefined;
dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.discord?.dm?.allowFrom) : undefined;
return {
...cfg,
channels: {
@@ -77,12 +69,7 @@ export const discordOnboardingAdapter: ChannelOnboardingAdapter = {
quickstartScore: configured ? 2 : 1,
};
},
configure: async ({
cfg,
prompter,
accountOverrides,
shouldPromptAccountIds,
}) => {
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
const discordOverride = accountOverrides.discord?.trim();
const defaultDiscordAccountId = resolveDefaultDiscordAccountId(cfg);
let discordAccountId = discordOverride
@@ -106,8 +93,7 @@ export const discordOnboardingAdapter: ChannelOnboardingAdapter = {
});
const accountConfigured = Boolean(resolvedAccount.token);
const allowEnv = discordAccountId === DEFAULT_ACCOUNT_ID;
const canUseEnv =
allowEnv && Boolean(process.env.DISCORD_BOT_TOKEN?.trim());
const canUseEnv = allowEnv && Boolean(process.env.DISCORD_BOT_TOKEN?.trim());
const hasConfigToken = Boolean(resolvedAccount.config.token);
let token: string | null = null;
@@ -178,9 +164,7 @@ export const discordOnboardingAdapter: ChannelOnboardingAdapter = {
...next.channels?.discord?.accounts,
[discordAccountId]: {
...next.channels?.discord?.accounts?.[discordAccountId],
enabled:
next.channels?.discord?.accounts?.[discordAccountId]
?.enabled ?? true,
enabled: next.channels?.discord?.accounts?.[discordAccountId]?.enabled ?? true,
token,
},
},

View File

@@ -1,18 +1,9 @@
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../../../routing/session-key.js";
import type {
PromptAccountId,
PromptAccountIdParams,
} from "../onboarding-types.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
import type { PromptAccountId, PromptAccountIdParams } from "../onboarding-types.js";
export const promptAccountId: PromptAccountId = async (
params: PromptAccountIdParams,
) => {
export const promptAccountId: PromptAccountId = async (params: PromptAccountIdParams) => {
const existingIds = params.listAccountIds(params.cfg);
const initial =
params.currentId?.trim() || params.defaultAccountId || DEFAULT_ACCOUNT_ID;
const initial = params.currentId?.trim() || params.defaultAccountId || DEFAULT_ACCOUNT_ID;
const choice = (await params.prompter.select({
message: `${params.label} account`,
options: [

View File

@@ -6,24 +6,16 @@ import {
resolveDefaultIMessageAccountId,
resolveIMessageAccount,
} from "../../../imessage/accounts.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../../../routing/session-key.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
import { formatDocsLink } from "../../../terminal/links.js";
import type {
ChannelOnboardingAdapter,
ChannelOnboardingDmPolicy,
} from "../onboarding-types.js";
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
const channel = "imessage" as const;
function setIMessageDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
const allowFrom =
dmPolicy === "open"
? addWildcardAllowFrom(cfg.channels?.imessage?.allowFrom)
: undefined;
dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.imessage?.allowFrom) : undefined;
return {
...cfg,
channels: {
@@ -53,10 +45,10 @@ export const imessageOnboardingAdapter: ChannelOnboardingAdapter = {
const account = resolveIMessageAccount({ cfg, accountId });
return Boolean(
account.config.cliPath ||
account.config.dbPath ||
account.config.allowFrom ||
account.config.service ||
account.config.region,
account.config.dbPath ||
account.config.allowFrom ||
account.config.service ||
account.config.region,
);
});
const imessageCliPath = cfg.channels?.imessage?.cliPath ?? "imsg";
@@ -72,12 +64,7 @@ export const imessageOnboardingAdapter: ChannelOnboardingAdapter = {
quickstartScore: imessageCliDetected ? 1 : 0,
};
},
configure: async ({
cfg,
prompter,
accountOverrides,
shouldPromptAccountIds,
}) => {
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
const imessageOverride = accountOverrides.imessage?.trim();
const defaultIMessageAccountId = resolveDefaultIMessageAccountId(cfg);
let imessageAccountId = imessageOverride
@@ -109,10 +96,7 @@ export const imessageOnboardingAdapter: ChannelOnboardingAdapter = {
});
resolvedCliPath = String(entered).trim();
if (!resolvedCliPath) {
await prompter.note(
"imsg CLI path required to enable iMessage.",
"iMessage",
);
await prompter.note("imsg CLI path required to enable iMessage.", "iMessage");
}
}
@@ -141,9 +125,7 @@ export const imessageOnboardingAdapter: ChannelOnboardingAdapter = {
...next.channels?.imessage?.accounts,
[imessageAccountId]: {
...next.channels?.imessage?.accounts?.[imessageAccountId],
enabled:
next.channels?.imessage?.accounts?.[imessageAccountId]
?.enabled ?? true,
enabled: next.channels?.imessage?.accounts?.[imessageAccountId]?.enabled ?? true,
cliPath: resolvedCliPath,
},
},

View File

@@ -4,10 +4,7 @@ import { resolveMSTeamsCredentials } from "../../../msteams/token.js";
import { DEFAULT_ACCOUNT_ID } from "../../../routing/session-key.js";
import { formatDocsLink } from "../../../terminal/links.js";
import type { WizardPrompter } from "../../../wizard/prompts.js";
import type {
ChannelOnboardingAdapter,
ChannelOnboardingDmPolicy,
} from "../onboarding-types.js";
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
import { addWildcardAllowFrom } from "./helpers.js";
const channel = "msteams" as const;
@@ -15,9 +12,7 @@ const channel = "msteams" as const;
function setMSTeamsDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
const allowFrom =
dmPolicy === "open"
? addWildcardAllowFrom(cfg.channels?.msteams?.allowFrom)?.map((entry) =>
String(entry),
)
? addWildcardAllowFrom(cfg.channels?.msteams?.allowFrom)?.map((entry) => String(entry))
: undefined;
return {
...cfg,
@@ -32,9 +27,7 @@ function setMSTeamsDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
};
}
async function noteMSTeamsCredentialHelp(
prompter: WizardPrompter,
): Promise<void> {
async function noteMSTeamsCredentialHelp(prompter: WizardPrompter): Promise<void> {
await prompter.note(
[
"1) Azure Bot registration → get App ID + Tenant ID",
@@ -59,15 +52,11 @@ const dmPolicy: ChannelOnboardingDmPolicy = {
export const msteamsOnboardingAdapter: ChannelOnboardingAdapter = {
channel,
getStatus: async ({ cfg }) => {
const configured = Boolean(
resolveMSTeamsCredentials(cfg.channels?.msteams),
);
const configured = Boolean(resolveMSTeamsCredentials(cfg.channels?.msteams));
return {
channel,
configured,
statusLines: [
`MS Teams: ${configured ? "configured" : "needs app credentials"}`,
],
statusLines: [`MS Teams: ${configured ? "configured" : "needs app credentials"}`],
selectionHint: configured ? "configured" : "needs app creds",
quickstartScore: configured ? 2 : 0,
};
@@ -76,14 +65,14 @@ export const msteamsOnboardingAdapter: ChannelOnboardingAdapter = {
const resolved = resolveMSTeamsCredentials(cfg.channels?.msteams);
const hasConfigCreds = Boolean(
cfg.channels?.msteams?.appId?.trim() &&
cfg.channels?.msteams?.appPassword?.trim() &&
cfg.channels?.msteams?.tenantId?.trim(),
cfg.channels?.msteams?.appPassword?.trim() &&
cfg.channels?.msteams?.tenantId?.trim(),
);
const canUseEnv = Boolean(
!hasConfigCreds &&
process.env.MSTEAMS_APP_ID?.trim() &&
process.env.MSTEAMS_APP_PASSWORD?.trim() &&
process.env.MSTEAMS_TENANT_ID?.trim(),
process.env.MSTEAMS_APP_ID?.trim() &&
process.env.MSTEAMS_APP_PASSWORD?.trim() &&
process.env.MSTEAMS_TENANT_ID?.trim(),
);
let next = cfg;

View File

@@ -2,29 +2,21 @@ import { detectBinary } from "../../../commands/onboard-helpers.js";
import { installSignalCli } from "../../../commands/signal-install.js";
import type { ClawdbotConfig } from "../../../config/config.js";
import type { DmPolicy } from "../../../config/types.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../../../routing/session-key.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
import {
listSignalAccountIds,
resolveDefaultSignalAccountId,
resolveSignalAccount,
} from "../../../signal/accounts.js";
import { formatDocsLink } from "../../../terminal/links.js";
import type {
ChannelOnboardingAdapter,
ChannelOnboardingDmPolicy,
} from "../onboarding-types.js";
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
const channel = "signal" as const;
function setSignalDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
const allowFrom =
dmPolicy === "open"
? addWildcardAllowFrom(cfg.channels?.signal?.allowFrom)
: undefined;
dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.signal?.allowFrom) : undefined;
return {
...cfg,
channels: {
@@ -62,9 +54,7 @@ export const signalOnboardingAdapter: ChannelOnboardingAdapter = {
`Signal: ${configured ? "configured" : "needs setup"}`,
`signal-cli: ${signalCliDetected ? "found" : "missing"} (${signalCliPath})`,
],
selectionHint: signalCliDetected
? "signal-cli found"
: "signal-cli missing",
selectionHint: signalCliDetected ? "signal-cli found" : "signal-cli missing",
quickstartScore: signalCliDetected ? 1 : 0,
};
},
@@ -113,21 +103,12 @@ export const signalOnboardingAdapter: ChannelOnboardingAdapter = {
if (result.ok && result.cliPath) {
cliDetected = true;
resolvedCliPath = result.cliPath;
await prompter.note(
`Installed signal-cli at ${result.cliPath}`,
"Signal",
);
await prompter.note(`Installed signal-cli at ${result.cliPath}`, "Signal");
} else if (!result.ok) {
await prompter.note(
result.error ?? "signal-cli install failed.",
"Signal",
);
await prompter.note(result.error ?? "signal-cli install failed.", "Signal");
}
} catch (err) {
await prompter.note(
`signal-cli install failed: ${String(err)}`,
"Signal",
);
await prompter.note(`signal-cli install failed: ${String(err)}`, "Signal");
}
}
}
@@ -183,9 +164,7 @@ export const signalOnboardingAdapter: ChannelOnboardingAdapter = {
...next.channels?.signal?.accounts,
[signalAccountId]: {
...next.channels?.signal?.accounts?.[signalAccountId],
enabled:
next.channels?.signal?.accounts?.[signalAccountId]
?.enabled ?? true,
enabled: next.channels?.signal?.accounts?.[signalAccountId]?.enabled ?? true,
account,
cliPath: resolvedCliPath ?? "signal-cli",
},

View File

@@ -1,9 +1,6 @@
import type { ClawdbotConfig } from "../../../config/config.js";
import type { DmPolicy } from "../../../config/types.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../../../routing/session-key.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
import {
listSlackAccountIds,
resolveDefaultSlackAccountId,
@@ -11,19 +8,14 @@ import {
} from "../../../slack/accounts.js";
import { formatDocsLink } from "../../../terminal/links.js";
import type { WizardPrompter } from "../../../wizard/prompts.js";
import type {
ChannelOnboardingAdapter,
ChannelOnboardingDmPolicy,
} from "../onboarding-types.js";
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
const channel = "slack" as const;
function setSlackDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
const allowFrom =
dmPolicy === "open"
? addWildcardAllowFrom(cfg.channels?.slack?.dm?.allowFrom)
: undefined;
dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.slack?.dm?.allowFrom) : undefined;
return {
...cfg,
channels: {
@@ -110,10 +102,7 @@ function buildSlackManifest(botName: string) {
return JSON.stringify(manifest, null, 2);
}
async function noteSlackTokenHelp(
prompter: WizardPrompter,
botName: string,
): Promise<void> {
async function noteSlackTokenHelp(prompter: WizardPrompter, botName: string): Promise<void> {
const manifest = buildSlackManifest(botName);
await prompter.note(
[
@@ -156,17 +145,10 @@ export const slackOnboardingAdapter: ChannelOnboardingAdapter = {
quickstartScore: configured ? 2 : 1,
};
},
configure: async ({
cfg,
prompter,
accountOverrides,
shouldPromptAccountIds,
}) => {
configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
const slackOverride = accountOverrides.slack?.trim();
const defaultSlackAccountId = resolveDefaultSlackAccountId(cfg);
let slackAccountId = slackOverride
? normalizeAccountId(slackOverride)
: defaultSlackAccountId;
let slackAccountId = slackOverride ? normalizeAccountId(slackOverride) : defaultSlackAccountId;
if (shouldPromptAccountIds && !slackOverride) {
slackAccountId = await promptAccountId({
cfg,
@@ -183,9 +165,7 @@ export const slackOnboardingAdapter: ChannelOnboardingAdapter = {
cfg: next,
accountId: slackAccountId,
});
const accountConfigured = Boolean(
resolvedAccount.botToken && resolvedAccount.appToken,
);
const accountConfigured = Boolean(resolvedAccount.botToken && resolvedAccount.appToken);
const allowEnv = slackAccountId === DEFAULT_ACCOUNT_ID;
const canUseEnv =
allowEnv &&
@@ -206,10 +186,7 @@ export const slackOnboardingAdapter: ChannelOnboardingAdapter = {
if (!accountConfigured) {
await noteSlackTokenHelp(prompter, slackBotName);
}
if (
canUseEnv &&
(!resolvedAccount.config.botToken || !resolvedAccount.config.appToken)
) {
if (canUseEnv && (!resolvedAccount.config.botToken || !resolvedAccount.config.appToken)) {
const keepEnv = await prompter.confirm({
message: "SLACK_BOT_TOKEN + SLACK_APP_TOKEN detected. Use env vars?",
initialValue: true,
@@ -296,9 +273,7 @@ export const slackOnboardingAdapter: ChannelOnboardingAdapter = {
...next.channels?.slack?.accounts,
[slackAccountId]: {
...next.channels?.slack?.accounts?.[slackAccountId],
enabled:
next.channels?.slack?.accounts?.[slackAccountId]?.enabled ??
true,
enabled: next.channels?.slack?.accounts?.[slackAccountId]?.enabled ?? true,
botToken,
appToken,
},

View File

@@ -1,9 +1,6 @@
import type { ClawdbotConfig } from "../../../config/config.js";
import type { DmPolicy } from "../../../config/types.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../../../routing/session-key.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
import {
listTelegramAccountIds,
resolveDefaultTelegramAccountId,
@@ -11,19 +8,14 @@ import {
} from "../../../telegram/accounts.js";
import { formatDocsLink } from "../../../terminal/links.js";
import type { WizardPrompter } from "../../../wizard/prompts.js";
import type {
ChannelOnboardingAdapter,
ChannelOnboardingDmPolicy,
} from "../onboarding-types.js";
import type { ChannelOnboardingAdapter, ChannelOnboardingDmPolicy } from "../onboarding-types.js";
import { addWildcardAllowFrom, promptAccountId } from "./helpers.js";
const channel = "telegram" as const;
function setTelegramDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) {
const allowFrom =
dmPolicy === "open"
? addWildcardAllowFrom(cfg.channels?.telegram?.allowFrom)
: undefined;
dmPolicy === "open" ? addWildcardAllowFrom(cfg.channels?.telegram?.allowFrom) : undefined;
return {
...cfg,
channels: {
@@ -62,9 +54,7 @@ async function promptTelegramAllowFrom(params: {
const entry = await prompter.text({
message: "Telegram allowFrom (user id)",
placeholder: "123456789",
initialValue: existingAllowFrom[0]
? String(existingAllowFrom[0])
: undefined,
initialValue: existingAllowFrom[0] ? String(existingAllowFrom[0]) : undefined,
validate: (value) => {
const raw = String(value ?? "").trim();
if (!raw) return "Required";
@@ -105,8 +95,7 @@ async function promptTelegramAllowFrom(params: {
...cfg.channels?.telegram?.accounts,
[accountId]: {
...cfg.channels?.telegram?.accounts?.[accountId],
enabled:
cfg.channels?.telegram?.accounts?.[accountId]?.enabled ?? true,
enabled: cfg.channels?.telegram?.accounts?.[accountId]?.enabled ?? true,
dmPolicy: "allowlist",
allowFrom: unique,
},
@@ -135,9 +124,7 @@ export const telegramOnboardingAdapter: ChannelOnboardingAdapter = {
channel,
configured,
statusLines: [`Telegram: ${configured ? "configured" : "needs token"}`],
selectionHint: configured
? "recommended · configured"
: "recommended · newcomer-friendly",
selectionHint: configured ? "recommended · configured" : "recommended · newcomer-friendly",
quickstartScore: configured ? 1 : 10,
};
},
@@ -171,8 +158,7 @@ export const telegramOnboardingAdapter: ChannelOnboardingAdapter = {
});
const accountConfigured = Boolean(resolvedAccount.token);
const allowEnv = telegramAccountId === DEFAULT_ACCOUNT_ID;
const canUseEnv =
allowEnv && Boolean(process.env.TELEGRAM_BOT_TOKEN?.trim());
const canUseEnv = allowEnv && Boolean(process.env.TELEGRAM_BOT_TOKEN?.trim());
const hasConfigToken = Boolean(
resolvedAccount.config.botToken || resolvedAccount.config.tokenFile,
);
@@ -252,9 +238,7 @@ export const telegramOnboardingAdapter: ChannelOnboardingAdapter = {
...next.channels?.telegram?.accounts,
[telegramAccountId]: {
...next.channels?.telegram?.accounts?.[telegramAccountId],
enabled:
next.channels?.telegram?.accounts?.[telegramAccountId]
?.enabled ?? true,
enabled: next.channels?.telegram?.accounts?.[telegramAccountId]?.enabled ?? true,
botToken: token,
},
},

View File

@@ -4,10 +4,7 @@ import { loginWeb } from "../../../channel-web.js";
import type { ClawdbotConfig } from "../../../config/config.js";
import { mergeWhatsAppConfig } from "../../../config/merge-config.js";
import type { DmPolicy } from "../../../config/types.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../../../routing/session-key.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../../routing/session-key.js";
import type { RuntimeEnv } from "../../../runtime.js";
import { formatDocsLink } from "../../../terminal/links.js";
import { normalizeE164 } from "../../../utils.js";
@@ -22,28 +19,15 @@ import { promptAccountId } from "./helpers.js";
const channel = "whatsapp" as const;
function setWhatsAppDmPolicy(
cfg: ClawdbotConfig,
dmPolicy: DmPolicy,
): ClawdbotConfig {
function setWhatsAppDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy): ClawdbotConfig {
return mergeWhatsAppConfig(cfg, { dmPolicy });
}
function setWhatsAppAllowFrom(
cfg: ClawdbotConfig,
allowFrom?: string[],
): ClawdbotConfig {
return mergeWhatsAppConfig(
cfg,
{ allowFrom },
{ unsetOnUndefined: ["allowFrom"] },
);
function setWhatsAppAllowFrom(cfg: ClawdbotConfig, allowFrom?: string[]): ClawdbotConfig {
return mergeWhatsAppConfig(cfg, { allowFrom }, { unsetOnUndefined: ["allowFrom"] });
}
function setMessagesResponsePrefix(
cfg: ClawdbotConfig,
responsePrefix?: string,
): ClawdbotConfig {
function setMessagesResponsePrefix(cfg: ClawdbotConfig, responsePrefix?: string): ClawdbotConfig {
return {
...cfg,
messages: {
@@ -53,10 +37,7 @@ function setMessagesResponsePrefix(
};
}
function setWhatsAppSelfChatMode(
cfg: ClawdbotConfig,
selfChatMode: boolean,
): ClawdbotConfig {
function setWhatsAppSelfChatMode(cfg: ClawdbotConfig, selfChatMode: boolean): ClawdbotConfig {
return mergeWhatsAppConfig(cfg, { selfChatMode });
}
@@ -69,10 +50,7 @@ async function pathExists(filePath: string): Promise<boolean> {
}
}
async function detectWhatsAppLinked(
cfg: ClawdbotConfig,
accountId: string,
): Promise<boolean> {
async function detectWhatsAppLinked(cfg: ClawdbotConfig, accountId: string): Promise<boolean> {
const { authDir } = resolveWhatsAppAuthDir({ cfg, accountId });
const credsPath = path.join(authDir, "creds.json");
return await pathExists(credsPath);
@@ -86,8 +64,7 @@ async function promptWhatsAppAllowFrom(
): Promise<ClawdbotConfig> {
const existingPolicy = cfg.channels?.whatsapp?.dmPolicy ?? "pairing";
const existingAllowFrom = cfg.channels?.whatsapp?.allowFrom ?? [];
const existingLabel =
existingAllowFrom.length > 0 ? existingAllowFrom.join(", ") : "unset";
const existingLabel = existingAllowFrom.length > 0 ? existingAllowFrom.join(", ") : "unset";
const existingResponsePrefix = cfg.messages?.responsePrefix;
if (options?.forceAllowlist) {
@@ -96,8 +73,7 @@ async function promptWhatsAppAllowFrom(
"WhatsApp number",
);
const entry = await prompter.text({
message:
"Your personal WhatsApp number (the phone you will message from)",
message: "Your personal WhatsApp number (the phone you will message from)",
placeholder: "+15555550123",
initialValue: existingAllowFrom[0],
validate: (value) => {
@@ -164,8 +140,7 @@ async function promptWhatsAppAllowFrom(
"WhatsApp number",
);
const entry = await prompter.text({
message:
"Your personal WhatsApp number (the phone you will message from)",
message: "Your personal WhatsApp number (the phone you will message from)",
placeholder: "+15555550123",
initialValue: existingAllowFrom[0],
validate: (value) => {
@@ -274,9 +249,7 @@ async function promptWhatsAppAllowFrom(
.split(/[\n,;]+/g)
.map((p) => p.trim())
.filter(Boolean);
const normalized = parts.map((part) =>
part === "*" ? "*" : normalizeE164(part),
);
const normalized = parts.map((part) => (part === "*" ? "*" : normalizeE164(part)));
const unique = [...new Set(normalized.filter(Boolean))];
next = setWhatsAppAllowFrom(next, unique);
}
@@ -289,18 +262,13 @@ export const whatsappOnboardingAdapter: ChannelOnboardingAdapter = {
getStatus: async ({ cfg, accountOverrides }) => {
const overrideId = accountOverrides.whatsapp?.trim();
const defaultAccountId = resolveDefaultWhatsAppAccountId(cfg);
const accountId = overrideId
? normalizeAccountId(overrideId)
: defaultAccountId;
const accountId = overrideId ? normalizeAccountId(overrideId) : defaultAccountId;
const linked = await detectWhatsAppLinked(cfg, accountId);
const accountLabel =
accountId === DEFAULT_ACCOUNT_ID ? "default" : accountId;
const accountLabel = accountId === DEFAULT_ACCOUNT_ID ? "default" : accountId;
return {
channel,
configured: linked,
statusLines: [
`WhatsApp (${accountLabel}): ${linked ? "linked" : "not linked"}`,
],
statusLines: [`WhatsApp (${accountLabel}): ${linked ? "linked" : "not linked"}`],
selectionHint: linked ? "linked" : "not linked",
quickstartScore: linked ? 5 : 4,
};
@@ -343,9 +311,7 @@ export const whatsappOnboardingAdapter: ChannelOnboardingAdapter = {
...next.channels?.whatsapp?.accounts,
[accountId]: {
...next.channels?.whatsapp?.accounts?.[accountId],
enabled:
next.channels?.whatsapp?.accounts?.[accountId]?.enabled ??
true,
enabled: next.channels?.whatsapp?.accounts?.[accountId]?.enabled ?? true,
},
},
},
@@ -370,9 +336,7 @@ export const whatsappOnboardingAdapter: ChannelOnboardingAdapter = {
);
}
const wantsLink = await prompter.confirm({
message: linked
? "WhatsApp already linked. Re-link now?"
: "Link WhatsApp now (QR)?",
message: linked ? "WhatsApp already linked. Re-link now?" : "Link WhatsApp now (QR)?",
initialValue: !linked,
});
if (wantsLink) {
@@ -380,16 +344,10 @@ export const whatsappOnboardingAdapter: ChannelOnboardingAdapter = {
await loginWeb(false, undefined, runtime, accountId);
} catch (err) {
runtime.error(`WhatsApp login failed: ${String(err)}`);
await prompter.note(
`Docs: ${formatDocsLink("/whatsapp", "whatsapp")}`,
"WhatsApp help",
);
await prompter.note(`Docs: ${formatDocsLink("/whatsapp", "whatsapp")}`, "WhatsApp help");
}
} else if (!linked) {
await prompter.note(
"Run `clawdbot channels login` later to link WhatsApp.",
"WhatsApp",
);
await prompter.note("Run `clawdbot channels login` later to link WhatsApp.", "WhatsApp");
}
next = await promptWhatsAppAllowFrom(next, runtime, prompter, {

View File

@@ -11,9 +11,7 @@ export const discordOutbound: ChannelOutboundAdapter = {
if (!trimmed) {
return {
ok: false,
error: new Error(
"Delivering to Discord requires --to <channelId|user:ID|channel:ID>",
),
error: new Error("Delivering to Discord requires --to <channelId|user:ID|channel:ID>"),
};
}
return { ok: true, to: trimmed };

View File

@@ -12,9 +12,7 @@ export const imessageOutbound: ChannelOutboundAdapter = {
if (!trimmed) {
return {
ok: false,
error: new Error(
"Delivering to iMessage requires --to <handle|chat_id:ID>",
),
error: new Error("Delivering to iMessage requires --to <handle|chat_id:ID>"),
};
}
return { ok: true, to: trimmed };

View File

@@ -21,17 +21,14 @@ export const msteamsOutbound: ChannelOutboundAdapter = {
return { ok: true, to: trimmed };
},
sendText: async ({ cfg, to, text, deps }) => {
const send =
deps?.sendMSTeams ??
((to, text) => sendMessageMSTeams({ cfg, to, text }));
const send = deps?.sendMSTeams ?? ((to, text) => sendMessageMSTeams({ cfg, to, text }));
const result = await send(to, text);
return { channel: "msteams", ...result };
},
sendMedia: async ({ cfg, to, text, mediaUrl, deps }) => {
const send =
deps?.sendMSTeams ??
((to, text, opts) =>
sendMessageMSTeams({ cfg, to, text, mediaUrl: opts?.mediaUrl }));
((to, text, opts) => sendMessageMSTeams({ cfg, to, text, mediaUrl: opts?.mediaUrl }));
const result = await send(to, text, { mediaUrl });
return { channel: "msteams", ...result };
},

View File

@@ -24,8 +24,7 @@ export const signalOutbound: ChannelOutboundAdapter = {
const maxBytes = resolveChannelMediaMaxBytes({
cfg,
resolveChannelLimitMb: ({ cfg, accountId }) =>
cfg.channels?.signal?.accounts?.[accountId]?.mediaMaxMb ??
cfg.channels?.signal?.mediaMaxMb,
cfg.channels?.signal?.accounts?.[accountId]?.mediaMaxMb ?? cfg.channels?.signal?.mediaMaxMb,
accountId,
});
const result = await send(to, text, {
@@ -39,8 +38,7 @@ export const signalOutbound: ChannelOutboundAdapter = {
const maxBytes = resolveChannelMediaMaxBytes({
cfg,
resolveChannelLimitMb: ({ cfg, accountId }) =>
cfg.channels?.signal?.accounts?.[accountId]?.mediaMaxMb ??
cfg.channels?.signal?.mediaMaxMb,
cfg.channels?.signal?.accounts?.[accountId]?.mediaMaxMb ?? cfg.channels?.signal?.mediaMaxMb,
accountId,
});
const result = await send(to, text, {

View File

@@ -10,9 +10,7 @@ export const slackOutbound: ChannelOutboundAdapter = {
if (!trimmed) {
return {
ok: false,
error: new Error(
"Delivering to Slack requires --to <channelId|user:ID|channel:ID>",
),
error: new Error("Delivering to Slack requires --to <channelId|user:ID|channel:ID>"),
};
}
return { ok: true, to: trimmed };

View File

@@ -33,15 +33,7 @@ export const telegramOutbound: ChannelOutboundAdapter = {
});
return { channel: "telegram", ...result };
},
sendMedia: async ({
to,
text,
mediaUrl,
accountId,
deps,
replyToId,
threadId,
}) => {
sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId, threadId }) => {
const send = deps?.sendTelegram ?? sendMessageTelegram;
const replyToMessageId = parseReplyToMessageId(replyToId);
const result = await send(to, text, {

View File

@@ -1,13 +1,7 @@
import { chunkText } from "../../../auto-reply/chunk.js";
import { shouldLogVerbose } from "../../../globals.js";
import {
sendMessageWhatsApp,
sendPollWhatsApp,
} from "../../../web/outbound.js";
import {
isWhatsAppGroupJid,
normalizeWhatsAppTarget,
} from "../../../whatsapp/normalize.js";
import { sendMessageWhatsApp, sendPollWhatsApp } from "../../../web/outbound.js";
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../../whatsapp/normalize.js";
import type { ChannelOutboundAdapter } from "../types.js";
export const whatsappOutbound: ChannelOutboundAdapter = {
@@ -17,9 +11,7 @@ export const whatsappOutbound: ChannelOutboundAdapter = {
pollMaxOptions: 12,
resolveTarget: ({ to, allowFrom, mode }) => {
const trimmed = to?.trim() ?? "";
const allowListRaw = (allowFrom ?? [])
.map((entry) => String(entry).trim())
.filter(Boolean);
const allowListRaw = (allowFrom ?? []).map((entry) => String(entry).trim()).filter(Boolean);
const hasWildcard = allowListRaw.includes("*");
const allowList = allowListRaw
.filter((entry) => entry !== "*")
@@ -29,10 +21,7 @@ export const whatsappOutbound: ChannelOutboundAdapter = {
if (trimmed) {
const normalizedTo = normalizeWhatsAppTarget(trimmed);
if (!normalizedTo) {
if (
(mode === "implicit" || mode === "heartbeat") &&
allowList.length > 0
) {
if ((mode === "implicit" || mode === "heartbeat") && allowList.length > 0) {
return { ok: true, to: allowList[0] };
}
return {

View File

@@ -15,16 +15,12 @@ export function listPairingChannels(): ChannelId[] {
.map((plugin) => plugin.id);
}
export function getPairingAdapter(
channelId: ChannelId,
): ChannelPairingAdapter | null {
export function getPairingAdapter(channelId: ChannelId): ChannelPairingAdapter | null {
const plugin = getChannelPlugin(channelId);
return plugin?.pairing ?? null;
}
export function requirePairingAdapter(
channelId: ChannelId,
): ChannelPairingAdapter {
export function requirePairingAdapter(channelId: ChannelId): ChannelPairingAdapter {
const adapter = getPairingAdapter(channelId);
if (!adapter) {
throw new Error(`Channel ${channelId} does not support pairing`);

View File

@@ -1,8 +1,5 @@
import type { ClawdbotConfig } from "../../config/config.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../../routing/session-key.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
type ChannelSectionBase = {
name?: string;
@@ -39,9 +36,7 @@ export function applyAccountNameToChannelSection(params: {
const channels = params.cfg.channels as Record<string, unknown> | undefined;
const baseConfig = channels?.[params.channelKey];
const base =
typeof baseConfig === "object" && baseConfig
? (baseConfig as ChannelSectionBase)
: undefined;
typeof baseConfig === "object" && baseConfig ? (baseConfig as ChannelSectionBase) : undefined;
const useAccounts = shouldStoreNameInAccounts({
cfg: params.cfg,
channelKey: params.channelKey,
@@ -61,10 +56,7 @@ export function applyAccountNameToChannelSection(params: {
},
} as ClawdbotConfig;
}
const baseAccounts: Record<
string,
Record<string, unknown>
> = base?.accounts ?? {};
const baseAccounts: Record<string, Record<string, unknown>> = base?.accounts ?? {};
const existingAccount = baseAccounts[accountId] ?? {};
const baseWithoutName =
accountId === DEFAULT_ACCOUNT_ID

View File

@@ -1,8 +1,5 @@
import { chunkText } from "../../auto-reply/chunk.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../../routing/session-key.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import {
listSignalAccountIds,
type ResolvedSignalAccount,
@@ -53,8 +50,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
reload: { configPrefixes: ["channels.signal"] },
config: {
listAccountIds: (cfg) => listSignalAccountIds(cfg),
resolveAccount: (cfg, accountId) =>
resolveSignalAccount({ cfg, accountId }),
resolveAccount: (cfg, accountId) => resolveSignalAccount({ cfg, accountId }),
defaultAccountId: (cfg) => resolveDefaultSignalAccountId(cfg),
setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({
@@ -69,14 +65,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
cfg,
sectionKey: "signal",
accountId,
clearBaseFields: [
"account",
"httpUrl",
"httpHost",
"httpPort",
"cliPath",
"name",
],
clearBaseFields: ["account", "httpUrl", "httpHost", "httpPort", "cliPath", "name"],
}),
isConfigured: (account) => account.configured,
describeAccount: (account) => ({
@@ -87,25 +76,20 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
baseUrl: account.baseUrl,
}),
resolveAllowFrom: ({ cfg, accountId }) =>
(resolveSignalAccount({ cfg, accountId }).config.allowFrom ?? []).map(
(entry) => String(entry),
(resolveSignalAccount({ cfg, accountId }).config.allowFrom ?? []).map((entry) =>
String(entry),
),
formatAllowFrom: ({ allowFrom }) =>
allowFrom
.map((entry) => String(entry).trim())
.filter(Boolean)
.map((entry) =>
entry === "*" ? "*" : normalizeE164(entry.replace(/^signal:/i, "")),
)
.map((entry) => (entry === "*" ? "*" : normalizeE164(entry.replace(/^signal:/i, ""))))
.filter(Boolean),
},
security: {
resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId =
accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
const useAccountPath = Boolean(
cfg.channels?.signal?.accounts?.[resolvedAccountId],
);
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
const useAccountPath = Boolean(cfg.channels?.signal?.accounts?.[resolvedAccountId]);
const basePath = useAccountPath
? `channels.signal.accounts.${resolvedAccountId}.`
: "channels.signal.";
@@ -115,8 +99,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
policyPath: `${basePath}dmPolicy`,
allowFromPath: basePath,
approveHint: formatPairingApproveHint("signal"),
normalizeEntry: (raw) =>
normalizeE164(raw.replace(/^signal:/i, "").trim()),
normalizeEntry: (raw) => normalizeE164(raw.replace(/^signal:/i, "").trim()),
};
},
collectWarnings: ({ account }) => {
@@ -264,8 +247,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
},
collectStatusIssues: (accounts) =>
accounts.flatMap((account) => {
const lastError =
typeof account.lastError === "string" ? account.lastError.trim() : "";
const lastError = typeof account.lastError === "string" ? account.lastError.trim() : "";
if (!lastError) return [];
return [
{
@@ -312,9 +294,7 @@ export const signalPlugin: ChannelPlugin<ResolvedSignalAccount> = {
accountId: account.accountId,
baseUrl: account.baseUrl,
});
ctx.log?.info(
`[${account.accountId}] starting provider (${account.baseUrl})`,
);
ctx.log?.info(`[${account.accountId}] starting provider (${account.baseUrl})`);
// Lazy import: the monitor pulls the reply pipeline; avoid ESM init cycles.
const { monitorSignalProvider } = await import("../../signal/index.js");
return monitorSignalProvider({

View File

@@ -1,12 +1,5 @@
import {
createActionGate,
readNumberParam,
readStringParam,
} from "../../agents/tools/common.js";
import {
handleSlackAction,
type SlackActionContext,
} from "../../agents/tools/slack-actions.js";
import { createActionGate, readNumberParam, readStringParam } from "../../agents/tools/common.js";
import { handleSlackAction, type SlackActionContext } from "../../agents/tools/slack-actions.js";
import { listEnabledSlackAccounts } from "../../slack/accounts.js";
import type {
ChannelMessageActionAdapter,
@@ -15,9 +8,7 @@ import type {
ChannelToolSend,
} from "./types.js";
export function createSlackActions(
providerId: string,
): ChannelMessageActionAdapter {
export function createSlackActions(providerId: string): ChannelMessageActionAdapter {
return {
listActions: ({ cfg }) => {
const accounts = listEnabledSlackAccounts(cfg).filter(
@@ -61,8 +52,7 @@ export function createSlackActions(
if (action !== "sendMessage") return null;
const to = typeof args.to === "string" ? args.to : undefined;
if (!to) return null;
const accountId =
typeof args.accountId === "string" ? args.accountId.trim() : undefined;
const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined;
return { to, accountId };
},
handleAction: async (ctx: ChannelMessageActionContext) => {
@@ -70,8 +60,7 @@ export function createSlackActions(
const accountId = ctx.accountId ?? undefined;
const toolContext = ctx.toolContext as SlackActionContext | undefined;
const resolveChannelId = () =>
readStringParam(params, "channelId") ??
readStringParam(params, "to", { required: true });
readStringParam(params, "channelId") ?? readStringParam(params, "to", { required: true });
if (action === "send") {
const to = readStringParam(params, "to", { required: true });
@@ -101,8 +90,7 @@ export function createSlackActions(
required: true,
});
const emoji = readStringParam(params, "emoji", { allowEmpty: true });
const remove =
typeof params.remove === "boolean" ? params.remove : undefined;
const remove = typeof params.remove === "boolean" ? params.remove : undefined;
return await handleSlackAction(
{
action: "react",
@@ -188,11 +176,7 @@ export function createSlackActions(
return await handleSlackAction(
{
action:
action === "pin"
? "pinMessage"
: action === "unpin"
? "unpinMessage"
: "listPins",
action === "pin" ? "pinMessage" : action === "unpin" ? "unpinMessage" : "listPins",
channelId: resolveChannelId(),
messageId,
accountId: accountId ?? undefined,
@@ -216,9 +200,7 @@ export function createSlackActions(
);
}
throw new Error(
`Action ${action} is not supported for provider ${providerId}.`,
);
throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
},
};
}

View File

@@ -1,7 +1,4 @@
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../../routing/session-key.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import {
listSlackAccountIds,
type ResolvedSlackAccount,
@@ -82,9 +79,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
appTokenSource: account.appTokenSource,
}),
resolveAllowFrom: ({ cfg, accountId }) =>
(resolveSlackAccount({ cfg, accountId }).dm?.allowFrom ?? []).map(
(entry) => String(entry),
),
(resolveSlackAccount({ cfg, accountId }).dm?.allowFrom ?? []).map((entry) => String(entry)),
formatAllowFrom: ({ allowFrom }) =>
allowFrom
.map((entry) => String(entry).trim())
@@ -93,11 +88,8 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
},
security: {
resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId =
accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
const useAccountPath = Boolean(
cfg.channels?.slack?.accounts?.[resolvedAccountId],
);
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
const useAccountPath = Boolean(cfg.channels?.slack?.accounts?.[resolvedAccountId]);
const allowFromPath = useAccountPath
? `channels.slack.accounts.${resolvedAccountId}.dm.`
: "channels.slack.dm.";
@@ -113,8 +105,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
const groupPolicy = account.config.groupPolicy ?? "allowlist";
if (groupPolicy !== "open") return [];
const channelAllowlistConfigured =
Boolean(account.config.channels) &&
Object.keys(account.config.channels ?? {}).length > 0;
Boolean(account.config.channels) && Object.keys(account.config.channels ?? {}).length > 0;
if (channelAllowlistConfigured) {
return [
`- Slack channels: groupPolicy="open" allows any channel not explicitly denied to trigger (mention-gated). Set channels.slack.groupPolicy="allowlist" and configure channels.slack.channels.`,
@@ -133,11 +124,8 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
resolveSlackAccount({ cfg, accountId }).replyToMode ?? "off",
allowTagsWhenOff: true,
buildToolContext: ({ cfg, accountId, context, hasRepliedRef }) => {
const configuredReplyToMode =
resolveSlackAccount({ cfg, accountId }).replyToMode ?? "off";
const effectiveReplyToMode = context.ThreadLabel
? "all"
: configuredReplyToMode;
const configuredReplyToMode = resolveSlackAccount({ cfg, accountId }).replyToMode ?? "off";
const effectiveReplyToMode = context.ThreadLabel ? "all" : configuredReplyToMode;
return {
currentChannelId: context.To?.startsWith("channel:")
? context.To.slice("channel:".length)
@@ -232,9 +220,7 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount> = {
if (!trimmed) {
return {
ok: false,
error: new Error(
"Delivering to Slack requires --to <channelId|user:ID|channel:ID>",
),
error: new Error("Delivering to Slack requires --to <channelId|user:ID|channel:ID>"),
};
}
return { ok: true, to: trimmed };

View File

@@ -27,9 +27,7 @@ type DiscordPermissionsAuditSummary = {
}>;
};
function readDiscordAccountStatus(
value: ChannelAccountSnapshot,
): DiscordAccountStatus | null {
function readDiscordAccountStatus(value: ChannelAccountSnapshot): DiscordAccountStatus | null {
if (!isRecord(value)) return null;
return {
accountId: value.accountId,
@@ -40,9 +38,7 @@ function readDiscordAccountStatus(
};
}
function readDiscordApplicationSummary(
value: unknown,
): DiscordApplicationSummary {
function readDiscordApplicationSummary(value: unknown): DiscordApplicationSummary {
if (!isRecord(value)) return {};
const intentsRaw = value.intents;
if (!isRecord(intentsRaw)) return {};
@@ -58,13 +54,10 @@ function readDiscordApplicationSummary(
};
}
function readDiscordPermissionsAuditSummary(
value: unknown,
): DiscordPermissionsAuditSummary {
function readDiscordPermissionsAuditSummary(value: unknown): DiscordPermissionsAuditSummary {
if (!isRecord(value)) return {};
const unresolvedChannels =
typeof value.unresolvedChannels === "number" &&
Number.isFinite(value.unresolvedChannels)
typeof value.unresolvedChannels === "number" && Number.isFinite(value.unresolvedChannels)
? value.unresolvedChannels
: undefined;
const channelsRaw = value.channels;
@@ -110,8 +103,7 @@ export function collectDiscordStatusIssues(
channel: "discord",
accountId,
kind: "intent",
message:
"Message Content Intent is disabled. Bot may not see normal channel messages.",
message: "Message Content Intent is disabled. Bot may not see normal channel messages.",
fix: "Enable Message Content Intent in Discord Dev Portal → Bot → Privileged Gateway Intents, or require mention-only operation.",
});
}
@@ -128,9 +120,7 @@ export function collectDiscordStatusIssues(
}
for (const channel of audit.channels ?? []) {
if (channel.ok === true) continue;
const missing = channel.missing?.length
? ` missing ${channel.missing.join(", ")}`
: "";
const missing = channel.missing?.length ? ` missing ${channel.missing.join(", ")}` : "";
const error = channel.error ? `: ${channel.error}` : "";
issues.push({
channel: "discord",

View File

@@ -1,7 +1,5 @@
export function asString(value: unknown): string | undefined {
return typeof value === "string" && value.trim().length > 0
? value.trim()
: undefined;
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
}
export function isRecord(value: unknown): value is Record<string, unknown> {

View File

@@ -20,9 +20,7 @@ type TelegramGroupMembershipAuditSummary = {
}>;
};
function readTelegramAccountStatus(
value: ChannelAccountSnapshot,
): TelegramAccountStatus | null {
function readTelegramAccountStatus(value: ChannelAccountSnapshot): TelegramAccountStatus | null {
if (!isRecord(value)) return null;
return {
accountId: value.accountId,
@@ -38,8 +36,7 @@ function readTelegramGroupMembershipAuditSummary(
): TelegramGroupMembershipAuditSummary {
if (!isRecord(value)) return {};
const unresolvedGroups =
typeof value.unresolvedGroups === "number" &&
Number.isFinite(value.unresolvedGroups)
typeof value.unresolvedGroups === "number" && Number.isFinite(value.unresolvedGroups)
? value.unresolvedGroups
: undefined;
const hasWildcardUnmentionedGroups =

View File

@@ -11,9 +11,7 @@ type WhatsAppAccountStatus = {
lastError?: unknown;
};
function readWhatsAppAccountStatus(
value: ChannelAccountSnapshot,
): WhatsAppAccountStatus | null {
function readWhatsAppAccountStatus(value: ChannelAccountSnapshot): WhatsAppAccountStatus | null {
if (!isRecord(value)) return null;
return {
accountId: value.accountId,
@@ -40,9 +38,7 @@ export function collectWhatsAppStatusIssues(
const running = account.running === true;
const connected = account.connected === true;
const reconnectAttempts =
typeof account.reconnectAttempts === "number"
? account.reconnectAttempts
: null;
typeof account.reconnectAttempts === "number" ? account.reconnectAttempts : null;
const lastError = asString(account.lastError);
if (!linked) {

View File

@@ -10,10 +10,7 @@ export async function buildChannelAccountSnapshot<ResolvedAccount>(params: {
probe?: unknown;
audit?: unknown;
}): Promise<ChannelAccountSnapshot> {
const account = params.plugin.config.resolveAccount(
params.cfg,
params.accountId,
);
const account = params.plugin.config.resolveAccount(params.cfg, params.accountId);
if (params.plugin.status?.buildAccountSnapshot) {
return await params.plugin.status.buildAccountSnapshot({
account,

View File

@@ -2,10 +2,7 @@ import { chunkMarkdownText } from "../../auto-reply/chunk.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { writeConfigFile } from "../../config/config.js";
import { shouldLogVerbose } from "../../globals.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../../routing/session-key.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import {
listTelegramAccountIds,
type ResolvedTelegramAccount,
@@ -66,8 +63,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
reload: { configPrefixes: ["channels.telegram"] },
config: {
listAccountIds: (cfg) => listTelegramAccountIds(cfg),
resolveAccount: (cfg, accountId) =>
resolveTelegramAccount({ cfg, accountId }),
resolveAccount: (cfg, accountId) => resolveTelegramAccount({ cfg, accountId }),
defaultAccountId: (cfg) => resolveDefaultTelegramAccountId(cfg),
setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({
@@ -93,8 +89,8 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
tokenSource: account.tokenSource,
}),
resolveAllowFrom: ({ cfg, accountId }) =>
(resolveTelegramAccount({ cfg, accountId }).config.allowFrom ?? []).map(
(entry) => String(entry),
(resolveTelegramAccount({ cfg, accountId }).config.allowFrom ?? []).map((entry) =>
String(entry),
),
formatAllowFrom: ({ allowFrom }) =>
allowFrom
@@ -105,11 +101,8 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
},
security: {
resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId =
accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
const useAccountPath = Boolean(
cfg.channels?.telegram?.accounts?.[resolvedAccountId],
);
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
const useAccountPath = Boolean(cfg.channels?.telegram?.accounts?.[resolvedAccountId]);
const basePath = useAccountPath
? `channels.telegram.accounts.${resolvedAccountId}.`
: "channels.telegram.";
@@ -141,8 +134,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
resolveRequireMention: resolveTelegramGroupRequireMention,
},
threading: {
resolveReplyToMode: ({ cfg }) =>
cfg.channels?.telegram?.replyToMode ?? "first",
resolveReplyToMode: ({ cfg }) => cfg.channels?.telegram?.replyToMode ?? "first",
},
messaging: {
normalizeTarget: normalizeTelegramMessagingTarget,
@@ -239,9 +231,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
},
sendText: async ({ to, text, accountId, deps, replyToId, threadId }) => {
const send = deps?.sendTelegram ?? sendMessageTelegram;
const replyToMessageId = replyToId
? Number.parseInt(replyToId, 10)
: undefined;
const replyToMessageId = replyToId ? Number.parseInt(replyToId, 10) : undefined;
const resolvedReplyToMessageId = Number.isFinite(replyToMessageId)
? replyToMessageId
: undefined;
@@ -253,19 +243,9 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
});
return { channel: "telegram", ...result };
},
sendMedia: async ({
to,
text,
mediaUrl,
accountId,
deps,
replyToId,
threadId,
}) => {
sendMedia: async ({ to, text, mediaUrl, accountId, deps, replyToId, threadId }) => {
const send = deps?.sendTelegram ?? sendMessageTelegram;
const replyToMessageId = replyToId
? Number.parseInt(replyToId, 10)
: undefined;
const replyToMessageId = replyToId ? Number.parseInt(replyToId, 10) : undefined;
const resolvedReplyToMessageId = Number.isFinite(replyToMessageId)
? replyToMessageId
: undefined;
@@ -307,11 +287,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
cfg.channels?.telegram?.groups;
const { groupIds, unresolvedGroups, hasWildcardUnmentionedGroups } =
collectTelegramUnmentionedGroupIds(groups);
if (
!groupIds.length &&
unresolvedGroups === 0 &&
!hasWildcardUnmentionedGroups
) {
if (!groupIds.length && unresolvedGroups === 0 && !hasWildcardUnmentionedGroups) {
return undefined;
}
const botId =
@@ -345,9 +321,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
cfg.channels?.telegram?.groups;
const allowUnmentionedGroups =
Boolean(
groups?.["*"] &&
(groups["*"] as { requireMention?: boolean }).requireMention ===
false,
groups?.["*"] && (groups["*"] as { requireMention?: boolean }).requireMention === false,
) ||
Object.entries(groups ?? {}).some(
([key, value]) =>
@@ -366,8 +340,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
lastStartAt: runtime?.lastStartAt ?? null,
lastStopAt: runtime?.lastStopAt ?? null,
lastError: runtime?.lastError ?? null,
mode:
runtime?.mode ?? (account.config.webhookUrl ? "webhook" : "polling"),
mode: runtime?.mode ?? (account.config.webhookUrl ? "webhook" : "polling"),
probe,
audit,
allowUnmentionedGroups,
@@ -387,18 +360,12 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
if (username) telegramBotLabel = ` (@${username})`;
} catch (err) {
if (shouldLogVerbose()) {
ctx.log?.debug?.(
`[${account.accountId}] bot probe failed: ${String(err)}`,
);
ctx.log?.debug?.(`[${account.accountId}] bot probe failed: ${String(err)}`);
}
}
ctx.log?.info(
`[${account.accountId}] starting provider${telegramBotLabel}`,
);
ctx.log?.info(`[${account.accountId}] starting provider${telegramBotLabel}`);
// Lazy import: the monitor pulls the reply pipeline; avoid ESM init cycles.
const { monitorTelegramProvider } = await import(
"../../telegram/monitor.js"
);
const { monitorTelegramProvider } = await import("../../telegram/monitor.js");
return monitorTelegramProvider({
token,
accountId: account.accountId,
@@ -414,9 +381,7 @@ export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount> = {
logoutAccount: async ({ accountId, cfg }) => {
const envToken = process.env.TELEGRAM_BOT_TOKEN?.trim() ?? "";
const nextCfg = { ...cfg } as ClawdbotConfig;
const nextTelegram = cfg.channels?.telegram
? { ...cfg.channels.telegram }
: undefined;
const nextTelegram = cfg.channels?.telegram ? { ...cfg.channels.telegram } : undefined;
let cleared = false;
let changed = false;
if (nextTelegram) {

View File

@@ -1,8 +1,5 @@
import type { ClawdbotConfig } from "../../config/config.js";
import type {
OutboundDeliveryResult,
OutboundSendDeps,
} from "../../infra/outbound/deliver.js";
import type { OutboundDeliveryResult, OutboundSendDeps } from "../../infra/outbound/deliver.js";
import type { RuntimeEnv } from "../../runtime.js";
import type {
ChannelAccountSnapshot,
@@ -20,10 +17,7 @@ import type {
} from "./types.core.js";
export type ChannelSetupAdapter = {
resolveAccountId?: (params: {
cfg: ClawdbotConfig;
accountId?: string;
}) => string;
resolveAccountId?: (params: { cfg: ClawdbotConfig; accountId?: string }) => string;
applyAccountName?: (params: {
cfg: ClawdbotConfig;
accountId: string;
@@ -43,34 +37,19 @@ export type ChannelSetupAdapter = {
export type ChannelConfigAdapter<ResolvedAccount> = {
listAccountIds: (cfg: ClawdbotConfig) => string[];
resolveAccount: (
cfg: ClawdbotConfig,
accountId?: string | null,
) => ResolvedAccount;
resolveAccount: (cfg: ClawdbotConfig, accountId?: string | null) => ResolvedAccount;
defaultAccountId?: (cfg: ClawdbotConfig) => string;
setAccountEnabled?: (params: {
cfg: ClawdbotConfig;
accountId: string;
enabled: boolean;
}) => ClawdbotConfig;
deleteAccount?: (params: {
cfg: ClawdbotConfig;
accountId: string;
}) => ClawdbotConfig;
deleteAccount?: (params: { cfg: ClawdbotConfig; accountId: string }) => ClawdbotConfig;
isEnabled?: (account: ResolvedAccount, cfg: ClawdbotConfig) => boolean;
disabledReason?: (account: ResolvedAccount, cfg: ClawdbotConfig) => string;
isConfigured?: (
account: ResolvedAccount,
cfg: ClawdbotConfig,
) => boolean | Promise<boolean>;
unconfiguredReason?: (
account: ResolvedAccount,
cfg: ClawdbotConfig,
) => string;
describeAccount?: (
account: ResolvedAccount,
cfg: ClawdbotConfig,
) => ChannelAccountSnapshot;
isConfigured?: (account: ResolvedAccount, cfg: ClawdbotConfig) => boolean | Promise<boolean>;
unconfiguredReason?: (account: ResolvedAccount, cfg: ClawdbotConfig) => string;
describeAccount?: (account: ResolvedAccount, cfg: ClawdbotConfig) => ChannelAccountSnapshot;
resolveAllowFrom?: (params: {
cfg: ClawdbotConfig;
accountId?: string | null;
@@ -154,9 +133,7 @@ export type ChannelStatusAdapter<ResolvedAccount> = {
configured: boolean;
enabled: boolean;
}) => ChannelAccountState;
collectStatusIssues?: (
accounts: ChannelAccountSnapshot[],
) => ChannelStatusIssue[];
collectStatusIssues?: (accounts: ChannelAccountSnapshot[]) => ChannelStatusIssue[];
};
export type ChannelGatewayContext<ResolvedAccount = unknown> = {
@@ -205,9 +182,7 @@ export type ChannelPairingAdapter = {
};
export type ChannelGatewayAdapter<ResolvedAccount = unknown> = {
startAccount?: (
ctx: ChannelGatewayContext<ResolvedAccount>,
) => Promise<unknown>;
startAccount?: (ctx: ChannelGatewayContext<ResolvedAccount>) => Promise<unknown>;
stopAccount?: (ctx: ChannelGatewayContext<ResolvedAccount>) => Promise<void>;
loginWithQrStart?: (params: {
accountId?: string;
@@ -219,9 +194,7 @@ export type ChannelGatewayAdapter<ResolvedAccount = unknown> = {
accountId?: string;
timeoutMs?: number;
}) => Promise<ChannelLoginWithQrWaitResult>;
logoutAccount?: (
ctx: ChannelLogoutContext<ResolvedAccount>,
) => Promise<ChannelLogoutResult>;
logoutAccount?: (ctx: ChannelLogoutContext<ResolvedAccount>) => Promise<ChannelLogoutResult>;
};
export type ChannelAuthAdapter = {
@@ -240,10 +213,10 @@ export type ChannelHeartbeatAdapter = {
accountId?: string | null;
deps?: ChannelHeartbeatDeps;
}) => Promise<{ ok: boolean; reason: string }>;
resolveRecipients?: (params: {
cfg: ClawdbotConfig;
opts?: { to?: string; all?: boolean };
}) => { recipients: string[]; source: string };
resolveRecipients?: (params: { cfg: ClawdbotConfig; opts?: { to?: string; all?: boolean } }) => {
recipients: string[];
source: string;
};
};
export type ChannelElevatedAdapter = {
@@ -262,7 +235,5 @@ export type ChannelSecurityAdapter<ResolvedAccount = unknown> = {
resolveDmPolicy?: (
ctx: ChannelSecurityContext<ResolvedAccount>,
) => ChannelSecurityDmPolicy | null;
collectWarnings?: (
ctx: ChannelSecurityContext<ResolvedAccount>,
) => Promise<string[]> | string[];
collectWarnings?: (ctx: ChannelSecurityContext<ResolvedAccount>) => Promise<string[]> | string[];
};

View File

@@ -3,10 +3,7 @@ import type { TSchema } from "@sinclair/typebox";
import type { MsgContext } from "../../auto-reply/templating.js";
import type { ClawdbotConfig } from "../../config/config.js";
import type { PollInput } from "../../polls.js";
import type {
GatewayClientMode,
GatewayClientName,
} from "../../utils/message-channel.js";
import type { GatewayClientMode, GatewayClientName } from "../../utils/message-channel.js";
import type { ChatChannelId } from "../registry.js";
import type { ChannelMessageActionName as ChannelMessageActionNameFromList } from "./message-action-names.js";
@@ -16,9 +13,7 @@ export type ChannelOutboundTargetMode = "explicit" | "implicit" | "heartbeat";
export type ChannelAgentTool = AgentTool<TSchema, unknown>;
export type ChannelAgentToolFactory = (params: {
cfg?: ClawdbotConfig;
}) => ChannelAgentTool[];
export type ChannelAgentToolFactory = (params: { cfg?: ClawdbotConfig }) => ChannelAgentTool[];
export type ChannelSetupInput = {
name?: string;
@@ -239,12 +234,8 @@ export type ChannelMessageActionAdapter = {
listActions?: (params: { cfg: ClawdbotConfig }) => ChannelMessageActionName[];
supportsAction?: (params: { action: ChannelMessageActionName }) => boolean;
supportsButtons?: (params: { cfg: ClawdbotConfig }) => boolean;
extractToolSend?: (params: {
args: Record<string, unknown>;
}) => ChannelToolSend | null;
handleAction?: (
ctx: ChannelMessageActionContext,
) => Promise<AgentToolResult<unknown>>;
extractToolSend?: (params: { args: Record<string, unknown> }) => ChannelToolSend | null;
handleAction?: (ctx: ChannelMessageActionContext) => Promise<AgentToolResult<unknown>>;
};
export type ChannelPollResult = {

View File

@@ -24,8 +24,7 @@ function getSessionRecipients(cfg: ClawdbotConfig) {
.filter(([key]) => !isGroupKey(key) && !isCronKey(key))
.map(([_, entry]) => ({
to:
normalizeChatChannelId(entry?.lastChannel) === "whatsapp" &&
entry?.lastTo
normalizeChatChannelId(entry?.lastChannel) === "whatsapp" && entry?.lastTo
? normalizeE164(entry.lastTo)
: "",
updatedAt: entry?.updatedAt ?? 0,
@@ -52,11 +51,8 @@ export function resolveWhatsAppHeartbeatRecipients(
const sessionRecipients = getSessionRecipients(cfg);
const allowFrom =
Array.isArray(cfg.channels?.whatsapp?.allowFrom) &&
cfg.channels.whatsapp.allowFrom.length > 0
? cfg.channels.whatsapp.allowFrom
.filter((v) => v !== "*")
.map(normalizeE164)
Array.isArray(cfg.channels?.whatsapp?.allowFrom) && cfg.channels.whatsapp.allowFrom.length > 0
? cfg.channels.whatsapp.allowFrom.filter((v) => v !== "*").map(normalizeE164)
: [];
const unique = (list: string[]) => [...new Set(list.filter(Boolean))];

View File

@@ -1,14 +1,8 @@
import {
createActionGate,
readStringParam,
} from "../../agents/tools/common.js";
import { createActionGate, readStringParam } from "../../agents/tools/common.js";
import { handleWhatsAppAction } from "../../agents/tools/whatsapp-actions.js";
import { chunkText } from "../../auto-reply/chunk.js";
import { shouldLogVerbose } from "../../globals.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../../routing/session-key.js";
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
import { normalizeE164 } from "../../utils.js";
import {
listWhatsAppAccountIds,
@@ -25,10 +19,7 @@ import {
webAuthExists,
} from "../../web/auth-store.js";
import { sendMessageWhatsApp, sendPollWhatsApp } from "../../web/outbound.js";
import {
isWhatsAppGroupJid,
normalizeWhatsAppTarget,
} from "../../whatsapp/normalize.js";
import { isWhatsAppGroupJid, normalizeWhatsAppTarget } from "../../whatsapp/normalize.js";
import { getChatChannelMeta } from "../registry.js";
import { createWhatsAppLoginTool } from "./agent-tools/whatsapp-login.js";
import { resolveWhatsAppGroupRequireMention } from "./group-mentions.js";
@@ -45,8 +36,7 @@ import { resolveWhatsAppHeartbeatRecipients } from "./whatsapp-heartbeat.js";
const meta = getChatChannelMeta("whatsapp");
const escapeRegExp = (value: string) =>
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
id: "whatsapp",
@@ -72,8 +62,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
gatewayMethods: ["web.login.start", "web.login.wait"],
config: {
listAccountIds: (cfg) => listWhatsAppAccountIds(cfg),
resolveAccount: (cfg, accountId) =>
resolveWhatsAppAccount({ cfg, accountId }),
resolveAccount: (cfg, accountId) => resolveWhatsAppAccount({ cfg, accountId }),
defaultAccountId: (cfg) => resolveDefaultWhatsAppAccountId(cfg),
setAccountEnabled: ({ cfg, accountId, enabled }) => {
const accountKey = accountId || DEFAULT_ACCOUNT_ID;
@@ -111,8 +100,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
},
};
},
isEnabled: (account, cfg) =>
account.enabled !== false && cfg.web?.enabled !== false,
isEnabled: (account, cfg) => account.enabled !== false && cfg.web?.enabled !== false,
disabledReason: () => "disabled",
isConfigured: async (account) => await webAuthExists(account.authDir),
unconfiguredReason: () => "not linked",
@@ -130,18 +118,13 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
allowFrom
.map((entry) => String(entry).trim())
.filter((entry): entry is string => Boolean(entry))
.map((entry) =>
entry === "*" ? entry : normalizeWhatsAppTarget(entry),
)
.map((entry) => (entry === "*" ? entry : normalizeWhatsAppTarget(entry)))
.filter((entry): entry is string => Boolean(entry)),
},
security: {
resolveDmPolicy: ({ cfg, accountId, account }) => {
const resolvedAccountId =
accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
const useAccountPath = Boolean(
cfg.channels?.whatsapp?.accounts?.[resolvedAccountId],
);
const resolvedAccountId = accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
const useAccountPath = Boolean(cfg.channels?.whatsapp?.accounts?.[resolvedAccountId]);
const basePath = useAccountPath
? `channels.whatsapp.accounts.${resolvedAccountId}.`
: "channels.whatsapp.";
@@ -244,29 +227,24 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
supportsAction: ({ action }) => action === "react",
handleAction: async ({ action, params, cfg, accountId }) => {
if (action !== "react") {
throw new Error(
`Action ${action} is not supported for provider ${meta.id}.`,
);
throw new Error(`Action ${action} is not supported for provider ${meta.id}.`);
}
const messageId = readStringParam(params, "messageId", {
required: true,
});
const emoji = readStringParam(params, "emoji", { allowEmpty: true });
const remove =
typeof params.remove === "boolean" ? params.remove : undefined;
const remove = typeof params.remove === "boolean" ? params.remove : undefined;
return await handleWhatsAppAction(
{
action: "react",
chatJid:
readStringParam(params, "chatJid") ??
readStringParam(params, "to", { required: true }),
readStringParam(params, "chatJid") ?? readStringParam(params, "to", { required: true }),
messageId,
emoji,
remove,
participant: readStringParam(params, "participant"),
accountId: accountId ?? undefined,
fromMe:
typeof params.fromMe === "boolean" ? params.fromMe : undefined,
fromMe: typeof params.fromMe === "boolean" ? params.fromMe : undefined,
},
cfg,
);
@@ -279,9 +257,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
pollMaxOptions: 12,
resolveTarget: ({ to, allowFrom, mode }) => {
const trimmed = to?.trim() ?? "";
const allowListRaw = (allowFrom ?? [])
.map((entry) => String(entry).trim())
.filter(Boolean);
const allowListRaw = (allowFrom ?? []).map((entry) => String(entry).trim()).filter(Boolean);
const hasWildcard = allowListRaw.includes("*");
const allowList = allowListRaw
.filter((entry) => entry !== "*")
@@ -291,10 +267,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
if (trimmed) {
const normalizedTo = normalizeWhatsAppTarget(trimmed);
if (!normalizedTo) {
if (
(mode === "implicit" || mode === "heartbeat") &&
allowList.length > 0
) {
if ((mode === "implicit" || mode === "heartbeat") && allowList.length > 0) {
return { ok: true, to: allowList[0] };
}
return {
@@ -356,8 +329,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
},
auth: {
login: async ({ cfg, accountId, runtime, verbose }) => {
const resolvedAccountId =
accountId?.trim() || resolveDefaultWhatsAppAccountId(cfg);
const resolvedAccountId = accountId?.trim() || resolveDefaultWhatsAppAccountId(cfg);
const { loginWeb } = await import("../../web/login.js");
await loginWeb(Boolean(verbose), undefined, runtime, resolvedAccountId);
},
@@ -368,9 +340,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
return { ok: false, reason: "whatsapp-disabled" };
}
const account = resolveWhatsAppAccount({ cfg, accountId });
const authExists = await (deps?.webAuthExists ?? webAuthExists)(
account.authDir,
);
const authExists = await (deps?.webAuthExists ?? webAuthExists)(account.authDir);
if (!authExists) {
return { ok: false, reason: "whatsapp-not-linked" };
}
@@ -382,8 +352,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
}
return { ok: true, reason: "ok" };
},
resolveRecipients: ({ cfg, opts }) =>
resolveWhatsAppHeartbeatRecipients(cfg, opts),
resolveRecipients: ({ cfg, opts }) => resolveWhatsAppHeartbeatRecipients(cfg, opts),
},
status: {
defaultRuntime: {
@@ -407,8 +376,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
? await webAuthExists(authDir)
: false;
const authAgeMs = linked && authDir ? getWebAuthAgeMs(authDir) : null;
const self =
linked && authDir ? readWebSelfId(authDir) : { e164: null, jid: null };
const self = linked && authDir ? readWebSelfId(authDir) : { e164: null, jid: null };
return {
configured: linked,
linked,
@@ -444,8 +412,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
allowFrom: account.allowFrom,
};
},
resolveAccountState: ({ configured }) =>
configured ? "linked" : "not linked",
resolveAccountState: ({ configured }) => (configured ? "linked" : "not linked"),
logSelfId: ({ account, runtime, includeChannelPrefix }) => {
logWebSelfId(account.authDir, runtime, includeChannelPrefix);
},
@@ -466,8 +433,7 @@ export const whatsappPlugin: ChannelPlugin<ResolvedWhatsAppAccount> = {
ctx.runtime,
ctx.abortSignal,
{
statusSink: (next) =>
ctx.setStatus({ accountId: ctx.accountId, ...next }),
statusSink: (next) => ctx.setStatus({ accountId: ctx.accountId, ...next }),
accountId: account.accountId,
},
);