refactor!: rename chat providers to channels

This commit is contained in:
Peter Steinberger
2026-01-13 06:16:43 +00:00
parent 0cd632ba84
commit 90342a4f3a
393 changed files with 8004 additions and 6737 deletions

View File

@@ -1,7 +1,7 @@
import crypto from "node:crypto";
import { callGateway } from "../../gateway/call.js";
import { INTERNAL_MESSAGE_PROVIDER } from "../../utils/message-provider.js";
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
import { AGENT_LANE_NESTED } from "../lanes.js";
import { extractAssistantText, stripToolMessages } from "./sessions-helpers.js";
@@ -25,7 +25,7 @@ export async function runAgentStep(params: {
message: string;
extraSystemPrompt: string;
timeoutMs: number;
provider?: string;
channel?: string;
lane?: string;
}): Promise<string | undefined> {
const stepIdem = crypto.randomUUID();
@@ -36,7 +36,7 @@ export async function runAgentStep(params: {
sessionKey: params.sessionKey,
idempotencyKey: stepIdem,
deliver: false,
provider: params.provider ?? INTERNAL_MESSAGE_PROVIDER,
channel: params.channel ?? INTERNAL_MESSAGE_CHANNEL,
lane: params.lane ?? AGENT_LANE_NESTED,
extraSystemPrompt: params.extraSystemPrompt,
},

View File

@@ -56,7 +56,7 @@ export async function handleDiscordAction(
cfg: ClawdbotConfig,
): Promise<AgentToolResult<unknown>> {
const action = readStringParam(params, "action", { required: true });
const isActionEnabled = createActionGate(cfg.discord?.actions);
const isActionEnabled = createActionGate(cfg.channels?.discord?.actions);
if (messagingActions.has(action)) {
return await handleDiscordMessagingAction(action, params, isActionEnabled);

View File

@@ -2,7 +2,7 @@ import { callGateway } from "../../gateway/call.js";
import {
GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES,
} from "../../utils/message-provider.js";
} from "../../utils/message-channel.js";
export const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789";

View File

@@ -1,5 +1,12 @@
import { Type } from "@sinclair/typebox";
import {
listChannelMessageActions,
supportsChannelMessageButtons,
} from "../../channels/plugins/message-actions.js";
import {
CHANNEL_MESSAGE_ACTION_NAMES,
type ChannelMessageActionName,
} from "../../channels/plugins/types.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { loadConfig } from "../../config/config.js";
import {
@@ -7,23 +14,15 @@ import {
GATEWAY_CLIENT_MODES,
} from "../../gateway/protocol/client-info.js";
import { runMessageAction } from "../../infra/outbound/message-action-runner.js";
import {
listProviderMessageActions,
supportsProviderMessageButtons,
} from "../../providers/plugins/message-actions.js";
import {
PROVIDER_MESSAGE_ACTION_NAMES,
type ProviderMessageActionName,
} from "../../providers/plugins/types.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import { stringEnum } from "../schema/typebox.js";
import type { AnyAgentTool } from "./common.js";
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
const AllMessageActions = PROVIDER_MESSAGE_ACTION_NAMES;
const AllMessageActions = CHANNEL_MESSAGE_ACTION_NAMES;
const MessageToolCommonSchema = {
provider: Type.Optional(Type.String()),
channel: Type.Optional(Type.String()),
to: Type.Optional(Type.String()),
message: Type.Optional(Type.String()),
media: Type.Optional(Type.String()),
@@ -131,8 +130,8 @@ type MessageToolOptions = {
};
function buildMessageToolSchema(cfg: ClawdbotConfig) {
const actions = listProviderMessageActions(cfg);
const includeButtons = supportsProviderMessageButtons(cfg);
const actions = listChannelMessageActions(cfg);
const includeButtons = supportsChannelMessageButtons(cfg);
return buildMessageToolSchemaFromActions(
actions.length > 0 ? actions : ["send"],
{ includeButtons },
@@ -155,14 +154,14 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
label: "Message",
name: "message",
description:
"Send messages and provider actions (polls, reactions, pins, threads, etc.) via configured provider plugins.",
"Send messages and channel actions (polls, reactions, pins, threads, etc.) via configured channel plugins.",
parameters: schema,
execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>;
const cfg = options?.config ?? loadConfig();
const action = readStringParam(params, "action", {
required: true,
}) as ProviderMessageActionName;
}) as ChannelMessageActionName;
const accountId = readStringParam(params, "accountId") ?? agentAccountId;
const gateway = {

View File

@@ -322,8 +322,8 @@ export function createSessionStatusTool(opts?: {
const queueSettings = resolveQueueSettings({
cfg,
provider:
resolved.entry.provider ?? resolved.entry.lastProvider ?? "unknown",
channel:
resolved.entry.channel ?? resolved.entry.lastChannel ?? "unknown",
sessionEntry: resolved.entry,
});
const queueKey = resolved.key ?? resolved.entry.sessionId;

View File

@@ -17,7 +17,7 @@ describe("resolveAnnounceTarget", () => {
sessionKey: "agent:main:discord:group:dev",
displayKey: "agent:main:discord:group:dev",
});
expect(target).toEqual({ provider: "discord", to: "channel:dev" });
expect(target).toEqual({ channel: "discord", to: "channel:dev" });
expect(callGatewayMock).not.toHaveBeenCalled();
});
@@ -26,7 +26,7 @@ describe("resolveAnnounceTarget", () => {
sessions: [
{
key: "agent:main:whatsapp:group:123@g.us",
lastProvider: "whatsapp",
lastChannel: "whatsapp",
lastTo: "123@g.us",
lastAccountId: "work",
},
@@ -38,7 +38,7 @@ describe("resolveAnnounceTarget", () => {
displayKey: "agent:main:whatsapp:group:123@g.us",
});
expect(target).toEqual({
provider: "whatsapp",
channel: "whatsapp",
to: "123@g.us",
accountId: "work",
});

View File

@@ -1,8 +1,8 @@
import { callGateway } from "../../gateway/call.js";
import {
getProviderPlugin,
normalizeProviderId,
} from "../../providers/plugins/index.js";
getChannelPlugin,
normalizeChannelId,
} from "../../channels/plugins/index.js";
import { callGateway } from "../../gateway/call.js";
import type { AnnounceTarget } from "./sessions-send-helpers.js";
import { resolveAnnounceTargetFromKey } from "./sessions-send-helpers.js";
@@ -15,8 +15,8 @@ export async function resolveAnnounceTarget(params: {
const fallback = parsed ?? parsedDisplay ?? null;
if (fallback) {
const normalized = normalizeProviderId(fallback.provider);
const plugin = normalized ? getProviderPlugin(normalized) : null;
const normalized = normalizeChannelId(fallback.channel);
const plugin = normalized ? getChannelPlugin(normalized) : null;
if (!plugin?.meta?.preferSessionLookupForAnnounceTarget) {
return fallback;
}
@@ -35,14 +35,14 @@ export async function resolveAnnounceTarget(params: {
const match =
sessions.find((entry) => entry?.key === params.sessionKey) ??
sessions.find((entry) => entry?.key === params.displayKey);
const provider =
typeof match?.lastProvider === "string" ? match.lastProvider : undefined;
const channel =
typeof match?.lastChannel === "string" ? match.lastChannel : undefined;
const to = typeof match?.lastTo === "string" ? match.lastTo : undefined;
const accountId =
typeof match?.lastAccountId === "string"
? match.lastAccountId
: undefined;
if (provider && to) return { provider, to, accountId };
if (channel && to) return { channel, to, accountId };
} catch {
// ignore
}

View File

@@ -56,11 +56,11 @@ export function classifySessionKind(params: {
return "other";
}
export function deriveProvider(params: {
export function deriveChannel(params: {
key: string;
kind: SessionKind;
provider?: string | null;
lastProvider?: string | null;
channel?: string | null;
lastChannel?: string | null;
}): string {
if (
params.kind === "cron" ||
@@ -68,10 +68,10 @@ export function deriveProvider(params: {
params.kind === "node"
)
return "internal";
const provider = normalizeKey(params.provider ?? undefined);
if (provider) return provider;
const lastProvider = normalizeKey(params.lastProvider ?? undefined);
if (lastProvider) return lastProvider;
const channel = normalizeKey(params.channel ?? undefined);
if (channel) return channel;
const lastChannel = normalizeKey(params.lastChannel ?? undefined);
if (lastChannel) return lastChannel;
const parts = params.key.split(":").filter(Boolean);
if (parts.length >= 3 && (parts[1] === "group" || parts[1] === "channel")) {
return parts[0];

View File

@@ -13,7 +13,7 @@ import type { AnyAgentTool } from "./common.js";
import { jsonResult, readStringArrayParam } from "./common.js";
import {
classifySessionKind,
deriveProvider,
deriveChannel,
resolveDisplaySessionKey,
resolveInternalSessionKey,
resolveMainSessionAlias,
@@ -24,7 +24,7 @@ import {
type SessionListRow = {
key: string;
kind: SessionKind;
provider: string;
channel: string;
label?: string;
displayName?: string;
updatedAt?: number | null;
@@ -37,7 +37,7 @@ type SessionListRow = {
systemSent?: boolean;
abortedLastRun?: boolean;
sendPolicy?: string;
lastProvider?: string;
lastChannel?: string;
lastTo?: string;
lastAccountId?: string;
transcriptPath?: string;
@@ -178,21 +178,19 @@ export function createSessionsListTool(opts?: {
mainKey,
});
const entryProvider =
typeof entry.provider === "string" ? entry.provider : undefined;
const lastProvider =
typeof entry.lastProvider === "string"
? entry.lastProvider
: undefined;
const entryChannel =
typeof entry.channel === "string" ? entry.channel : undefined;
const lastChannel =
typeof entry.lastChannel === "string" ? entry.lastChannel : undefined;
const lastAccountId =
typeof entry.lastAccountId === "string"
? entry.lastAccountId
: undefined;
const derivedProvider = deriveProvider({
const derivedChannel = deriveChannel({
key,
kind,
provider: entryProvider,
lastProvider,
channel: entryChannel,
lastChannel,
});
const sessionId =
@@ -205,7 +203,7 @@ export function createSessionsListTool(opts?: {
const row: SessionListRow = {
key: displayKey,
kind,
provider: derivedProvider,
channel: derivedChannel,
label: typeof entry.label === "string" ? entry.label : undefined,
displayName:
typeof entry.displayName === "string"
@@ -241,7 +239,7 @@ export function createSessionsListTool(opts?: {
: undefined,
sendPolicy:
typeof entry.sendPolicy === "string" ? entry.sendPolicy : undefined,
lastProvider,
lastChannel,
lastTo: typeof entry.lastTo === "string" ? entry.lastTo : undefined,
lastAccountId,
transcriptPath,

View File

@@ -1,8 +1,8 @@
import type { ClawdbotConfig } from "../../config/config.js";
import {
getProviderPlugin,
normalizeProviderId,
} from "../../providers/plugins/index.js";
getChannelPlugin,
normalizeChannelId,
} from "../../channels/plugins/index.js";
import type { ClawdbotConfig } from "../../config/config.js";
const ANNOUNCE_SKIP_TOKEN = "ANNOUNCE_SKIP";
const REPLY_SKIP_TOKEN = "REPLY_SKIP";
@@ -10,7 +10,7 @@ const DEFAULT_PING_PONG_TURNS = 5;
const MAX_PING_PONG_TURNS = 5;
export type AnnounceTarget = {
provider: string;
channel: string;
to: string;
accountId?: string;
};
@@ -24,29 +24,29 @@ export function resolveAnnounceTargetFromKey(
? rawParts.slice(2)
: rawParts;
if (parts.length < 3) return null;
const [providerRaw, kind, ...rest] = parts;
const [channelRaw, kind, ...rest] = parts;
if (kind !== "group" && kind !== "channel") return null;
const id = rest.join(":").trim();
if (!id) return null;
if (!providerRaw) return null;
const normalizedProvider = normalizeProviderId(providerRaw);
const provider = normalizedProvider ?? providerRaw.toLowerCase();
const kindTarget = normalizedProvider
if (!channelRaw) return null;
const normalizedChannel = normalizeChannelId(channelRaw);
const channel = normalizedChannel ?? channelRaw.toLowerCase();
const kindTarget = normalizedChannel
? kind === "channel"
? `channel:${id}`
: `group:${id}`
: id;
const normalized = normalizedProvider
? getProviderPlugin(normalizedProvider)?.messaging?.normalizeTarget?.(
const normalized = normalizedChannel
? getChannelPlugin(normalizedChannel)?.messaging?.normalizeTarget?.(
kindTarget,
)
: undefined;
return { provider, to: normalized ?? kindTarget };
return { channel, to: normalized ?? kindTarget };
}
export function buildAgentToAgentMessageContext(params: {
requesterSessionKey?: string;
requesterProvider?: string;
requesterChannel?: string;
targetSessionKey: string;
}) {
const lines = [
@@ -54,8 +54,8 @@ export function buildAgentToAgentMessageContext(params: {
params.requesterSessionKey
? `Agent 1 (requester) session: ${params.requesterSessionKey}.`
: undefined,
params.requesterProvider
? `Agent 1 (requester) provider: ${params.requesterProvider}.`
params.requesterChannel
? `Agent 1 (requester) channel: ${params.requesterChannel}.`
: undefined,
`Agent 2 (target) session: ${params.targetSessionKey}.`,
].filter(Boolean);
@@ -64,9 +64,9 @@ export function buildAgentToAgentMessageContext(params: {
export function buildAgentToAgentReplyContext(params: {
requesterSessionKey?: string;
requesterProvider?: string;
requesterChannel?: string;
targetSessionKey: string;
targetProvider?: string;
targetChannel?: string;
currentRole: "requester" | "target";
turn: number;
maxTurns: number;
@@ -82,12 +82,12 @@ export function buildAgentToAgentReplyContext(params: {
params.requesterSessionKey
? `Agent 1 (requester) session: ${params.requesterSessionKey}.`
: undefined,
params.requesterProvider
? `Agent 1 (requester) provider: ${params.requesterProvider}.`
params.requesterChannel
? `Agent 1 (requester) channel: ${params.requesterChannel}.`
: undefined,
`Agent 2 (target) session: ${params.targetSessionKey}.`,
params.targetProvider
? `Agent 2 (target) provider: ${params.targetProvider}.`
params.targetChannel
? `Agent 2 (target) channel: ${params.targetChannel}.`
: undefined,
`If you want to stop the ping-pong, reply exactly "${REPLY_SKIP_TOKEN}".`,
].filter(Boolean);
@@ -96,9 +96,9 @@ export function buildAgentToAgentReplyContext(params: {
export function buildAgentToAgentAnnounceContext(params: {
requesterSessionKey?: string;
requesterProvider?: string;
requesterChannel?: string;
targetSessionKey: string;
targetProvider?: string;
targetChannel?: string;
originalMessage: string;
roundOneReply?: string;
latestReply?: string;
@@ -108,12 +108,12 @@ export function buildAgentToAgentAnnounceContext(params: {
params.requesterSessionKey
? `Agent 1 (requester) session: ${params.requesterSessionKey}.`
: undefined,
params.requesterProvider
? `Agent 1 (requester) provider: ${params.requesterProvider}.`
params.requesterChannel
? `Agent 1 (requester) channel: ${params.requesterChannel}.`
: undefined,
`Agent 2 (target) session: ${params.targetSessionKey}.`,
params.targetProvider
? `Agent 2 (target) provider: ${params.targetProvider}.`
params.targetChannel
? `Agent 2 (target) channel: ${params.targetChannel}.`
: undefined,
`Original request: ${params.originalMessage}`,
params.roundOneReply
@@ -123,7 +123,7 @@ export function buildAgentToAgentAnnounceContext(params: {
? `Latest reply: ${params.latestReply}`
: "Latest reply: (not available).",
`If you want to remain silent, reply exactly "${ANNOUNCE_SKIP_TOKEN}".`,
"Any other reply will be posted to the target provider.",
"Any other reply will be posted to the target channel.",
"After this reply, the agent-to-agent conversation is over.",
].filter(Boolean);
return lines.join("\n");

View File

@@ -28,7 +28,7 @@ describe("sessions_send gating", () => {
it("blocks cross-agent sends when tools.agentToAgent.enabled is false", async () => {
const tool = createSessionsSendTool({
agentSessionKey: "agent:main:main",
agentProvider: "whatsapp",
agentChannel: "whatsapp",
});
const result = await tool.execute("call1", {

View File

@@ -13,9 +13,9 @@ import {
} from "../../routing/session-key.js";
import { SESSION_LABEL_MAX_LENGTH } from "../../sessions/session-label.js";
import {
type GatewayMessageProvider,
INTERNAL_MESSAGE_PROVIDER,
} from "../../utils/message-provider.js";
type GatewayMessageChannel,
INTERNAL_MESSAGE_CHANNEL,
} from "../../utils/message-channel.js";
import { AGENT_LANE_NESTED } from "../lanes.js";
import { readLatestAssistantReply, runAgentStep } from "./agent-step.js";
import type { AnyAgentTool } from "./common.js";
@@ -51,7 +51,7 @@ const SessionsSendToolSchema = Type.Object({
export function createSessionsSendTool(opts?: {
agentSessionKey?: string;
agentProvider?: GatewayMessageProvider;
agentChannel?: GatewayMessageChannel;
sandboxed?: boolean;
}): AnyAgentTool {
return {
@@ -297,7 +297,7 @@ export function createSessionsSendTool(opts?: {
const agentMessageContext = buildAgentToAgentMessageContext({
requesterSessionKey: opts?.agentSessionKey,
requesterProvider: opts?.agentProvider,
requesterChannel: opts?.agentChannel,
targetSessionKey: displayKey,
});
const sendParams = {
@@ -305,12 +305,12 @@ export function createSessionsSendTool(opts?: {
sessionKey: resolvedKey,
idempotencyKey,
deliver: false,
provider: INTERNAL_MESSAGE_PROVIDER,
channel: INTERNAL_MESSAGE_CHANNEL,
lane: AGENT_LANE_NESTED,
extraSystemPrompt: agentMessageContext,
};
const requesterSessionKey = opts?.agentSessionKey;
const requesterProvider = opts?.agentProvider;
const requesterChannel = opts?.agentChannel;
const maxPingPongTurns = resolvePingPongTurns(cfg);
const delivery = { status: "pending", mode: "announce" as const };
@@ -344,7 +344,7 @@ export function createSessionsSendTool(opts?: {
sessionKey: resolvedKey,
displayKey,
});
const targetProvider = announceTarget?.provider ?? "unknown";
const targetChannel = announceTarget?.channel ?? "unknown";
if (
maxPingPongTurns > 0 &&
requesterSessionKey &&
@@ -360,9 +360,9 @@ export function createSessionsSendTool(opts?: {
: "target";
const replyPrompt = buildAgentToAgentReplyContext({
requesterSessionKey,
requesterProvider,
requesterChannel,
targetSessionKey: displayKey,
targetProvider,
targetChannel,
currentRole,
turn,
maxTurns: maxPingPongTurns,
@@ -386,9 +386,9 @@ export function createSessionsSendTool(opts?: {
}
const announcePrompt = buildAgentToAgentAnnounceContext({
requesterSessionKey,
requesterProvider,
requesterChannel,
targetSessionKey: displayKey,
targetProvider,
targetChannel,
originalMessage: message,
roundOneReply: primaryReply,
latestReply,
@@ -412,7 +412,7 @@ export function createSessionsSendTool(opts?: {
params: {
to: announceTarget.to,
message: announceReply.trim(),
provider: announceTarget.provider,
channel: announceTarget.channel,
accountId: announceTarget.accountId,
idempotencyKey: crypto.randomUUID(),
},
@@ -421,7 +421,7 @@ export function createSessionsSendTool(opts?: {
} catch (err) {
log.warn("sessions_send announce delivery failed", {
runId: runContextId,
provider: announceTarget.provider,
channel: announceTarget.channel,
to: announceTarget.to,
error: formatErrorMessage(err),
});

View File

@@ -9,7 +9,7 @@ import {
normalizeAgentId,
parseAgentSessionKey,
} from "../../routing/session-key.js";
import type { GatewayMessageProvider } from "../../utils/message-provider.js";
import type { GatewayMessageChannel } from "../../utils/message-channel.js";
import { resolveAgentConfig } from "../agent-scope.js";
import { AGENT_LANE_SUBAGENT } from "../lanes.js";
import { optionalStringEnum } from "../schema/typebox.js";
@@ -47,7 +47,7 @@ function normalizeModelSelection(value: unknown): string | undefined {
export function createSessionsSpawnTool(opts?: {
agentSessionKey?: string;
agentProvider?: GatewayMessageProvider;
agentChannel?: GatewayMessageChannel;
sandboxed?: boolean;
}): AnyAgentTool {
return {
@@ -174,7 +174,7 @@ export function createSessionsSpawnTool(opts?: {
}
const childSystemPrompt = buildSubagentSystemPrompt({
requesterSessionKey,
requesterProvider: opts?.agentProvider,
requesterChannel: opts?.agentChannel,
childSessionKey,
label: label || undefined,
task,
@@ -188,7 +188,7 @@ export function createSessionsSpawnTool(opts?: {
params: {
message: task,
sessionKey: childSessionKey,
provider: opts?.agentProvider,
channel: opts?.agentChannel,
idempotencyKey: childIdem,
deliver: false,
lane: AGENT_LANE_SUBAGENT,
@@ -221,7 +221,7 @@ export function createSessionsSpawnTool(opts?: {
runId: childRunId,
childSessionKey,
requesterSessionKey: requesterInternalKey,
requesterProvider: opts?.agentProvider,
requesterChannel: opts?.agentChannel,
requesterDisplayKey,
task,
cleanup,

View File

@@ -36,7 +36,7 @@ vi.mock("../../slack/actions.js", () => ({
describe("handleSlackAction", () => {
it("adds reactions", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await handleSlackAction(
{
action: "react",
@@ -50,7 +50,7 @@ describe("handleSlackAction", () => {
});
it("removes reactions on empty emoji", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await handleSlackAction(
{
action: "react",
@@ -64,7 +64,7 @@ describe("handleSlackAction", () => {
});
it("removes reactions when remove flag set", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await handleSlackAction(
{
action: "react",
@@ -79,7 +79,7 @@ describe("handleSlackAction", () => {
});
it("rejects removes without emoji", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await expect(
handleSlackAction(
{
@@ -96,7 +96,7 @@ describe("handleSlackAction", () => {
it("respects reaction gating", async () => {
const cfg = {
slack: { botToken: "tok", actions: { reactions: false } },
channels: { slack: { botToken: "tok", actions: { reactions: false } } },
} as ClawdbotConfig;
await expect(
handleSlackAction(
@@ -112,7 +112,7 @@ describe("handleSlackAction", () => {
});
it("passes threadTs to sendSlackMessage for thread replies", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
await handleSlackAction(
{
action: "sendMessage",
@@ -133,7 +133,7 @@ describe("handleSlackAction", () => {
});
it("auto-injects threadTs from context when replyToMode=all", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
await handleSlackAction(
{
@@ -159,7 +159,7 @@ describe("handleSlackAction", () => {
});
it("replyToMode=first threads first message then stops", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
const hasRepliedRef = { value: false };
const context = {
@@ -198,7 +198,7 @@ describe("handleSlackAction", () => {
});
it("replyToMode=first marks hasRepliedRef even when threadTs is explicit", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
const hasRepliedRef = { value: false };
const context = {
@@ -244,7 +244,7 @@ describe("handleSlackAction", () => {
});
it("replyToMode=first without hasRepliedRef does not thread", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
await handleSlackAction(
{ action: "sendMessage", to: "channel:C123", content: "No ref" },
@@ -263,7 +263,7 @@ describe("handleSlackAction", () => {
});
it("does not auto-inject threadTs when replyToMode=off", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
await handleSlackAction(
{
@@ -285,7 +285,7 @@ describe("handleSlackAction", () => {
});
it("does not auto-inject threadTs when sending to different channel", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
await handleSlackAction(
{
@@ -311,7 +311,7 @@ describe("handleSlackAction", () => {
});
it("explicit threadTs overrides context threadTs", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
await handleSlackAction(
{
@@ -338,7 +338,7 @@ describe("handleSlackAction", () => {
});
it("handles channel target without prefix when replyToMode=all", async () => {
const cfg = { slack: { botToken: "tok" } } as ClawdbotConfig;
const cfg = { channels: { slack: { botToken: "tok" } } } as ClawdbotConfig;
sendSlackMessage.mockClear();
await handleSlackAction(
{

View File

@@ -93,7 +93,7 @@ export async function handleSlackAction(
const accountId = readStringParam(params, "accountId");
const accountOpts = accountId ? { accountId } : undefined;
const account = resolveSlackAccount({ cfg, accountId });
const actionConfig = account.actions ?? cfg.slack?.actions;
const actionConfig = account.actions ?? cfg.channels?.slack?.actions;
const isActionEnabled = createActionGate(actionConfig);
if (reactionsActions.has(action)) {

View File

@@ -34,7 +34,9 @@ describe("handleTelegramAction", () => {
});
it("adds reactions", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
await handleTelegramAction(
{
action: "react",
@@ -53,7 +55,9 @@ describe("handleTelegramAction", () => {
});
it("removes reactions on empty emoji", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
await handleTelegramAction(
{
action: "react",
@@ -72,7 +76,9 @@ describe("handleTelegramAction", () => {
});
it("removes reactions when remove flag set", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
await handleTelegramAction(
{
action: "react",
@@ -93,7 +99,9 @@ describe("handleTelegramAction", () => {
it("respects reaction gating", async () => {
const cfg = {
telegram: { botToken: "tok", actions: { reactions: false } },
channels: {
telegram: { botToken: "tok", actions: { reactions: false } },
},
} as ClawdbotConfig;
await expect(
handleTelegramAction(
@@ -109,7 +117,9 @@ describe("handleTelegramAction", () => {
});
it("sends a text message", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
const result = await handleTelegramAction(
{
action: "sendMessage",
@@ -130,7 +140,9 @@ describe("handleTelegramAction", () => {
});
it("sends a message with media", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
await handleTelegramAction(
{
action: "sendMessage",
@@ -152,7 +164,9 @@ describe("handleTelegramAction", () => {
it("respects sendMessage gating", async () => {
const cfg = {
telegram: { botToken: "tok", actions: { sendMessage: false } },
channels: {
telegram: { botToken: "tok", actions: { sendMessage: false } },
},
} as ClawdbotConfig;
await expect(
handleTelegramAction(
@@ -182,7 +196,9 @@ describe("handleTelegramAction", () => {
});
it("requires inlineButtons capability when buttons are provided", async () => {
const cfg = { telegram: { botToken: "tok" } } as ClawdbotConfig;
const cfg = {
channels: { telegram: { botToken: "tok" } },
} as ClawdbotConfig;
await expect(
handleTelegramAction(
{
@@ -198,7 +214,9 @@ describe("handleTelegramAction", () => {
it("sends messages with inline keyboard buttons when enabled", async () => {
const cfg = {
telegram: { botToken: "tok", capabilities: ["inlineButtons"] },
channels: {
telegram: { botToken: "tok", capabilities: ["inlineButtons"] },
},
} as ClawdbotConfig;
await handleTelegramAction(
{

View File

@@ -1,7 +1,6 @@
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
import { resolveChannelCapabilities } from "../../config/channel-capabilities.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { resolveProviderCapabilities } from "../../config/provider-capabilities.js";
import {
reactMessageTelegram,
sendMessageTelegram,
@@ -26,9 +25,9 @@ function hasInlineButtonsCapability(params: {
accountId?: string | undefined;
}): boolean {
const caps =
resolveProviderCapabilities({
resolveChannelCapabilities({
cfg: params.cfg,
provider: "telegram",
channel: "telegram",
accountId: params.accountId,
}) ?? [];
return caps.some((cap) => cap.toLowerCase() === "inlinebuttons");
@@ -84,7 +83,7 @@ export async function handleTelegramAction(
): Promise<AgentToolResult<unknown>> {
const action = readStringParam(params, "action", { required: true });
const accountId = readStringParam(params, "accountId");
const isActionEnabled = createActionGate(cfg.telegram?.actions);
const isActionEnabled = createActionGate(cfg.channels?.telegram?.actions);
if (action === "react") {
if (!isActionEnabled("reactions")) {
@@ -103,7 +102,7 @@ export async function handleTelegramAction(
const token = resolveTelegramToken(cfg, { accountId }).token;
if (!token) {
throw new Error(
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or telegram.botToken.",
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
);
}
await reactMessageTelegram(chatId ?? "", messageId ?? 0, emoji ?? "", {
@@ -130,7 +129,7 @@ export async function handleTelegramAction(
!hasInlineButtonsCapability({ cfg, accountId: accountId ?? undefined })
) {
throw new Error(
'Telegram inline buttons requested but not enabled. Add "inlineButtons" to telegram.capabilities (or telegram.accounts.<id>.capabilities).',
'Telegram inline buttons requested but not enabled. Add "inlineButtons" to channels.telegram.capabilities (or channels.telegram.accounts.<id>.capabilities).',
);
}
// Optional threading parameters for forum topics and reply chains
@@ -143,7 +142,7 @@ export async function handleTelegramAction(
const token = resolveTelegramToken(cfg, { accountId }).token;
if (!token) {
throw new Error(
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or telegram.botToken.",
"Telegram bot token missing. Set TELEGRAM_BOT_TOKEN or channels.telegram.botToken.",
);
}
const result = await sendMessageTelegram(to, content, {

View File

@@ -10,7 +10,7 @@ vi.mock("../../web/outbound.js", () => ({
}));
const enabledConfig = {
whatsapp: { actions: { reactions: true } },
channels: { whatsapp: { actions: { reactions: true } } },
} as ClawdbotConfig;
describe("handleWhatsAppAction", () => {
@@ -112,7 +112,7 @@ describe("handleWhatsAppAction", () => {
it("respects reaction gating", async () => {
const cfg = {
whatsapp: { actions: { reactions: false } },
channels: { whatsapp: { actions: { reactions: false } } },
} as ClawdbotConfig;
await expect(
handleWhatsAppAction(

View File

@@ -14,7 +14,7 @@ export async function handleWhatsAppAction(
cfg: ClawdbotConfig,
): Promise<AgentToolResult<unknown>> {
const action = readStringParam(params, "action", { required: true });
const isActionEnabled = createActionGate(cfg.whatsapp?.actions);
const isActionEnabled = createActionGate(cfg.channels?.whatsapp?.actions);
if (action === "react") {
if (!isActionEnabled("reactions")) {