refactor: unify message tool + CLI

This commit is contained in:
Peter Steinberger
2026-01-13 00:12:05 +00:00
parent 103003d9ff
commit 3636a2bf51
20 changed files with 838 additions and 1380 deletions

View File

@@ -1,283 +0,0 @@
import { Type } from "@sinclair/typebox";
import { createReactionSchema } from "./reaction-schema.js";
export const DiscordToolSchema = Type.Union([
createReactionSchema({
ids: {
channelId: Type.String(),
messageId: Type.String(),
},
includeRemove: true,
}),
Type.Object({
action: Type.Literal("reactions"),
channelId: Type.String(),
messageId: Type.String(),
limit: Type.Optional(Type.Number()),
}),
Type.Object({
action: Type.Literal("sticker"),
to: Type.String(),
stickerIds: Type.Array(Type.String()),
content: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("poll"),
to: Type.String(),
question: Type.String(),
answers: Type.Array(Type.String()),
allowMultiselect: Type.Optional(Type.Boolean()),
durationHours: Type.Optional(Type.Number()),
content: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("permissions"),
channelId: Type.String(),
}),
Type.Union([
Type.Object({
action: Type.Literal("fetchMessage"),
messageLink: Type.String(),
guildId: Type.Optional(Type.String()),
channelId: Type.Optional(Type.String()),
messageId: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("fetchMessage"),
guildId: Type.String(),
channelId: Type.String(),
messageId: Type.String(),
}),
]),
Type.Object({
action: Type.Literal("readMessages"),
channelId: Type.String(),
limit: Type.Optional(Type.Number()),
before: Type.Optional(Type.String()),
after: Type.Optional(Type.String()),
around: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("sendMessage"),
to: Type.String(),
content: Type.String(),
mediaUrl: Type.Optional(Type.String()),
replyTo: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("editMessage"),
channelId: Type.String(),
messageId: Type.String(),
content: Type.String(),
}),
Type.Object({
action: Type.Literal("deleteMessage"),
channelId: Type.String(),
messageId: Type.String(),
}),
Type.Object({
action: Type.Literal("threadCreate"),
channelId: Type.String(),
name: Type.String(),
messageId: Type.Optional(Type.String()),
autoArchiveMinutes: Type.Optional(Type.Number()),
}),
Type.Object({
action: Type.Literal("threadList"),
guildId: Type.String(),
channelId: Type.Optional(Type.String()),
includeArchived: Type.Optional(Type.Boolean()),
before: Type.Optional(Type.String()),
limit: Type.Optional(Type.Number()),
}),
Type.Object({
action: Type.Literal("threadReply"),
channelId: Type.String(),
content: Type.String(),
mediaUrl: Type.Optional(Type.String()),
replyTo: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("pinMessage"),
channelId: Type.String(),
messageId: Type.String(),
}),
Type.Object({
action: Type.Literal("unpinMessage"),
channelId: Type.String(),
messageId: Type.String(),
}),
Type.Object({
action: Type.Literal("listPins"),
channelId: Type.String(),
}),
Type.Object({
action: Type.Literal("searchMessages"),
guildId: Type.String(),
content: Type.String(),
channelId: Type.Optional(Type.String()),
channelIds: Type.Optional(Type.Array(Type.String())),
authorId: Type.Optional(Type.String()),
authorIds: Type.Optional(Type.Array(Type.String())),
limit: Type.Optional(Type.Number()),
}),
Type.Object({
action: Type.Literal("memberInfo"),
guildId: Type.String(),
userId: Type.String(),
}),
Type.Object({
action: Type.Literal("roleInfo"),
guildId: Type.String(),
}),
Type.Object({
action: Type.Literal("emojiList"),
guildId: Type.String(),
}),
Type.Object({
action: Type.Literal("emojiUpload"),
guildId: Type.String(),
name: Type.String(),
mediaUrl: Type.String(),
roleIds: Type.Optional(Type.Array(Type.String())),
}),
Type.Object({
action: Type.Literal("stickerUpload"),
guildId: Type.String(),
name: Type.String(),
description: Type.String(),
tags: Type.String(),
mediaUrl: Type.String(),
}),
Type.Object({
action: Type.Literal("roleAdd"),
guildId: Type.String(),
userId: Type.String(),
roleId: Type.String(),
}),
Type.Object({
action: Type.Literal("roleRemove"),
guildId: Type.String(),
userId: Type.String(),
roleId: Type.String(),
}),
Type.Object({
action: Type.Literal("channelInfo"),
channelId: Type.String(),
}),
Type.Object({
action: Type.Literal("channelList"),
guildId: Type.String(),
}),
Type.Object({
action: Type.Literal("voiceStatus"),
guildId: Type.String(),
userId: Type.String(),
}),
Type.Object({
action: Type.Literal("eventList"),
guildId: Type.String(),
}),
Type.Object({
action: Type.Literal("eventCreate"),
guildId: Type.String(),
name: Type.String(),
startTime: Type.String(),
endTime: Type.Optional(Type.String()),
description: Type.Optional(Type.String()),
channelId: Type.Optional(Type.String()),
entityType: Type.Optional(
Type.Union([
Type.Literal("voice"),
Type.Literal("stage"),
Type.Literal("external"),
]),
),
location: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("timeout"),
guildId: Type.String(),
userId: Type.String(),
durationMinutes: Type.Optional(Type.Number()),
until: Type.Optional(Type.String()),
reason: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("kick"),
guildId: Type.String(),
userId: Type.String(),
reason: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("ban"),
guildId: Type.String(),
userId: Type.String(),
reason: Type.Optional(Type.String()),
deleteMessageDays: Type.Optional(Type.Number()),
}),
// Channel management actions
Type.Object({
action: Type.Literal("channelCreate"),
guildId: Type.String(),
name: Type.String(),
type: Type.Optional(Type.Number()),
parentId: Type.Optional(Type.String()),
topic: Type.Optional(Type.String()),
position: Type.Optional(Type.Number()),
nsfw: Type.Optional(Type.Boolean()),
}),
Type.Object({
action: Type.Literal("channelEdit"),
channelId: Type.String(),
name: Type.Optional(Type.String()),
topic: Type.Optional(Type.String()),
position: Type.Optional(Type.Number()),
parentId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
nsfw: Type.Optional(Type.Boolean()),
rateLimitPerUser: Type.Optional(Type.Number()),
}),
Type.Object({
action: Type.Literal("channelDelete"),
channelId: Type.String(),
}),
Type.Object({
action: Type.Literal("channelMove"),
guildId: Type.String(),
channelId: Type.String(),
parentId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
position: Type.Optional(Type.Number()),
}),
// Category management actions (convenience aliases)
Type.Object({
action: Type.Literal("categoryCreate"),
guildId: Type.String(),
name: Type.String(),
position: Type.Optional(Type.Number()),
}),
Type.Object({
action: Type.Literal("categoryEdit"),
categoryId: Type.String(),
name: Type.Optional(Type.String()),
position: Type.Optional(Type.Number()),
}),
Type.Object({
action: Type.Literal("categoryDelete"),
categoryId: Type.String(),
}),
// Permission overwrite actions
Type.Object({
action: Type.Literal("channelPermissionSet"),
channelId: Type.String(),
targetId: Type.String(),
targetType: Type.Union([Type.Literal("role"), Type.Literal("member")]),
allow: Type.Optional(Type.String()),
deny: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("channelPermissionRemove"),
channelId: Type.String(),
targetId: Type.String(),
}),
]);

View File

@@ -1,18 +0,0 @@
import { loadConfig } from "../../config/config.js";
import type { AnyAgentTool } from "./common.js";
import { handleDiscordAction } from "./discord-actions.js";
import { DiscordToolSchema } from "./discord-schema.js";
export function createDiscordTool(): AnyAgentTool {
return {
label: "Discord",
name: "discord",
description: "Manage Discord messages, reactions, and moderation.",
parameters: DiscordToolSchema,
execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>;
const cfg = loadConfig();
return await handleDiscordAction(params, cfg);
},
};
}

View File

@@ -1,74 +1,25 @@
import { Type } from "@sinclair/typebox";
import { parseReplyDirectives } from "../../auto-reply/reply/reply-directives.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { loadConfig } from "../../config/config.js";
import { runMessageAction } from "../../infra/outbound/message-action-runner.js";
import {
type MessagePollResult,
type MessageSendResult,
sendMessage,
sendPoll,
} from "../../infra/outbound/message.js";
import { resolveMessageProviderSelection } from "../../infra/outbound/provider-selection.js";
import {
dispatchProviderMessageAction,
listProviderMessageActions,
supportsProviderMessageButtons,
} from "../../providers/plugins/message-actions.js";
import type { ProviderMessageActionName } from "../../providers/plugins/types.js";
import {
PROVIDER_MESSAGE_ACTION_NAMES,
type ProviderMessageActionName,
} from "../../providers/plugins/types.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import {
GATEWAY_CLIENT_MODES,
GATEWAY_CLIENT_NAMES,
} from "../../utils/message-provider.js";
import type { AnyAgentTool } from "./common.js";
import {
jsonResult,
readNumberParam,
readStringArrayParam,
readStringParam,
} from "./common.js";
import { jsonResult, readNumberParam, readStringParam } from "./common.js";
const AllMessageActions = [
"send",
"poll",
"react",
"reactions",
"read",
"edit",
"delete",
"pin",
"unpin",
"list-pins",
"permissions",
"thread-create",
"thread-list",
"thread-reply",
"search",
"sticker",
"member-info",
"role-info",
"emoji-list",
"emoji-upload",
"sticker-upload",
"role-add",
"role-remove",
"channel-info",
"channel-list",
"channel-create",
"channel-edit",
"channel-delete",
"channel-move",
"category-create",
"category-edit",
"category-delete",
"voice-status",
"event-list",
"event-create",
"timeout",
"kick",
"ban",
];
const AllMessageActions = PROVIDER_MESSAGE_ACTION_NAMES;
const MessageToolCommonSchema = {
provider: Type.Optional(Type.String()),
@@ -148,7 +99,7 @@ const MessageToolCommonSchema = {
};
function buildMessageToolSchemaFromActions(
actions: string[],
actions: readonly string[],
options: { includeButtons: boolean },
) {
const props: Record<string, unknown> = { ...MessageToolCommonSchema };
@@ -227,14 +178,7 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
const action = readStringParam(params, "action", {
required: true,
}) as ProviderMessageActionName;
const providerSelection = await resolveMessageProviderSelection({
cfg,
provider: readStringParam(params, "provider"),
});
const provider = providerSelection.provider;
const accountId = readStringParam(params, "accountId") ?? agentAccountId;
const dryRun = Boolean(params.dryRun);
const gateway = {
url: readStringParam(params, "gatewayUrl", { trim: false }),
@@ -258,144 +202,17 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
}
: undefined;
if (action === "send") {
const to = readStringParam(params, "to", { required: true });
let message = readStringParam(params, "message", {
required: true,
allowEmpty: true,
});
// Let send accept the same inline directives we use elsewhere.
// Provider plugins consume `replyTo` / `media` / `buttons` from params.
const parsed = parseReplyDirectives(message);
message = parsed.text;
params.message = message;
if (!params.replyTo && parsed.replyToId)
params.replyTo = parsed.replyToId;
if (!params.media) {
params.media = parsed.mediaUrls?.[0] || parsed.mediaUrl || undefined;
}
const mediaUrl = readStringParam(params, "media", { trim: false });
const gifPlayback =
typeof params.gifPlayback === "boolean" ? params.gifPlayback : false;
const bestEffort =
typeof params.bestEffort === "boolean"
? params.bestEffort
: undefined;
if (dryRun) {
const result: MessageSendResult = await sendMessage({
to,
content: message,
mediaUrl: mediaUrl || undefined,
provider: provider || undefined,
accountId: accountId ?? undefined,
gifPlayback,
dryRun,
bestEffort,
gateway,
});
return jsonResult(result);
}
const handled = await dispatchProviderMessageAction({
provider,
action,
cfg,
params,
accountId,
gateway,
toolContext,
dryRun,
});
if (handled) return handled;
const result: MessageSendResult = await sendMessage({
to,
content: message,
mediaUrl: mediaUrl || undefined,
provider: provider || undefined,
accountId: accountId ?? undefined,
gifPlayback,
dryRun,
bestEffort,
gateway,
});
return jsonResult(result);
}
if (action === "poll") {
const to = readStringParam(params, "to", { required: true });
const question = readStringParam(params, "pollQuestion", {
required: true,
});
const options =
readStringArrayParam(params, "pollOption", { required: true }) ?? [];
const allowMultiselect =
typeof params.pollMulti === "boolean" ? params.pollMulti : undefined;
const durationHours = readNumberParam(params, "pollDurationHours", {
integer: true,
});
const maxSelections = allowMultiselect
? Math.max(2, options.length)
: 1;
if (dryRun) {
const result: MessagePollResult = await sendPoll({
to,
question,
options,
maxSelections,
durationHours: durationHours ?? undefined,
provider,
dryRun,
gateway,
});
return jsonResult(result);
}
const handled = await dispatchProviderMessageAction({
provider,
action,
cfg,
params,
accountId,
gateway,
toolContext,
dryRun,
});
if (handled) return handled;
const result: MessagePollResult = await sendPoll({
to,
question,
options,
maxSelections,
durationHours: durationHours ?? undefined,
provider,
dryRun,
gateway,
});
return jsonResult(result);
}
const handled = await dispatchProviderMessageAction({
provider,
action,
const result = await runMessageAction({
cfg,
action,
params,
accountId,
defaultAccountId: accountId ?? undefined,
gateway,
toolContext,
dryRun,
});
if (handled) return handled;
throw new Error(
`Message action ${action} not supported for provider ${provider}.`,
);
if (result.toolResult) return result.toolResult;
return jsonResult(result.payload);
},
};
}

View File

@@ -1,24 +0,0 @@
import { type TSchema, Type } from "@sinclair/typebox";
type ReactionSchemaOptions = {
action?: string;
ids: Record<string, TSchema>;
emoji?: TSchema;
includeRemove?: boolean;
extras?: Record<string, TSchema>;
};
export function createReactionSchema(options: ReactionSchemaOptions) {
const schema: Record<string, TSchema> = {
action: Type.Literal(options.action ?? "react"),
...options.ids,
emoji: options.emoji ?? Type.String(),
};
if (options.includeRemove) {
schema.remove = Type.Optional(Type.Boolean());
}
if (options.extras) {
Object.assign(schema, options.extras);
}
return Type.Object(schema);
}

View File

@@ -1,77 +0,0 @@
import { Type } from "@sinclair/typebox";
import { createReactionSchema } from "./reaction-schema.js";
export const SlackToolSchema = Type.Union([
createReactionSchema({
ids: {
channelId: Type.String(),
messageId: Type.String(),
},
includeRemove: true,
extras: {
accountId: Type.Optional(Type.String()),
},
}),
Type.Object({
action: Type.Literal("reactions"),
channelId: Type.String(),
messageId: Type.String(),
accountId: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("sendMessage"),
to: Type.String(),
content: Type.String(),
mediaUrl: Type.Optional(Type.String()),
threadTs: Type.Optional(Type.String()),
accountId: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("editMessage"),
channelId: Type.String(),
messageId: Type.String(),
content: Type.String(),
accountId: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("deleteMessage"),
channelId: Type.String(),
messageId: Type.String(),
accountId: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("readMessages"),
channelId: Type.String(),
limit: Type.Optional(Type.Number()),
before: Type.Optional(Type.String()),
after: Type.Optional(Type.String()),
accountId: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("pinMessage"),
channelId: Type.String(),
messageId: Type.String(),
accountId: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("unpinMessage"),
channelId: Type.String(),
messageId: Type.String(),
accountId: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("listPins"),
channelId: Type.String(),
accountId: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("memberInfo"),
userId: Type.String(),
accountId: Type.Optional(Type.String()),
}),
Type.Object({
action: Type.Literal("emojiList"),
accountId: Type.Optional(Type.String()),
}),
]);

View File

@@ -1,85 +0,0 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const handleSlackActionMock = vi.fn();
vi.mock("./slack-actions.js", () => ({
handleSlackAction: (params: unknown, cfg: unknown) =>
handleSlackActionMock(params, cfg),
}));
import { createSlackTool } from "./slack-tool.js";
describe("slack tool", () => {
beforeEach(() => {
handleSlackActionMock.mockReset();
handleSlackActionMock.mockResolvedValue({
content: [],
details: { ok: true },
});
});
it("injects agentAccountId when accountId is missing", async () => {
const tool = createSlackTool({
agentAccountId: " Kev ",
config: { slack: { accounts: { kev: {} } } },
});
await tool.execute("call-1", {
action: "sendMessage",
to: "channel:C1",
content: "hello",
});
expect(handleSlackActionMock).toHaveBeenCalledTimes(1);
const [params] = handleSlackActionMock.mock.calls[0] ?? [];
expect(params).toMatchObject({ accountId: "kev" });
});
it("keeps explicit accountId when provided", async () => {
const tool = createSlackTool({
agentAccountId: "kev",
config: {},
});
await tool.execute("call-2", {
action: "sendMessage",
to: "channel:C1",
content: "hello",
accountId: "rex",
});
expect(handleSlackActionMock).toHaveBeenCalledTimes(1);
const [params] = handleSlackActionMock.mock.calls[0] ?? [];
expect(params).toMatchObject({ accountId: "rex" });
});
it("does not inject accountId when agentAccountId is missing", async () => {
const tool = createSlackTool({ config: {} });
await tool.execute("call-3", {
action: "sendMessage",
to: "channel:C1",
content: "hello",
});
expect(handleSlackActionMock).toHaveBeenCalledTimes(1);
const [params] = handleSlackActionMock.mock.calls[0] ?? [];
expect(params).not.toHaveProperty("accountId");
});
it("does not inject unknown agentAccountId when not configured", async () => {
const tool = createSlackTool({
agentAccountId: "unknown",
config: { slack: { accounts: { kev: {} } } },
});
await tool.execute("call-4", {
action: "sendMessage",
to: "channel:C1",
content: "hello",
});
const [params] = handleSlackActionMock.mock.calls[0] ?? [];
expect(params).not.toHaveProperty("accountId");
});
});

View File

@@ -1,82 +0,0 @@
import type { ClawdbotConfig } from "../../config/config.js";
import { loadConfig } from "../../config/config.js";
import { logVerbose } from "../../globals.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import type { AnyAgentTool } from "./common.js";
import { handleSlackAction } from "./slack-actions.js";
import { SlackToolSchema } from "./slack-schema.js";
type SlackToolOptions = {
agentAccountId?: string;
config?: ClawdbotConfig;
/** Current channel ID for auto-threading. */
currentChannelId?: string;
/** Current thread timestamp for auto-threading. */
currentThreadTs?: string;
/** Reply-to mode for auto-threading. */
replyToMode?: "off" | "first" | "all";
/** Mutable ref to track if a reply was sent (for "first" mode). */
hasRepliedRef?: { value: boolean };
};
function resolveAgentAccountId(value?: string): string | undefined {
const trimmed = value?.trim();
if (!trimmed) return undefined;
return normalizeAccountId(trimmed);
}
function resolveConfiguredAccountId(
cfg: ClawdbotConfig,
accountId: string,
): string | undefined {
if (accountId === "default") return accountId;
const accounts = cfg.slack?.accounts;
if (!accounts || typeof accounts !== "object") return undefined;
if (accountId in accounts) return accountId;
const match = Object.keys(accounts).find(
(key) => key.toLowerCase() === accountId.toLowerCase(),
);
return match;
}
function hasAccountId(params: Record<string, unknown>): boolean {
const raw = params.accountId;
if (typeof raw !== "string") return false;
return raw.trim().length > 0;
}
export function createSlackTool(options?: SlackToolOptions): AnyAgentTool {
const agentAccountId = resolveAgentAccountId(options?.agentAccountId);
return {
label: "Slack",
name: "slack",
description: "Manage Slack messages, reactions, and pins.",
parameters: SlackToolSchema,
execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>;
const cfg = options?.config ?? loadConfig();
const resolvedAccountId = agentAccountId
? resolveConfiguredAccountId(cfg, agentAccountId)
: undefined;
const resolvedParams =
resolvedAccountId && !hasAccountId(params)
? { ...params, accountId: resolvedAccountId }
: params;
if (hasAccountId(resolvedParams)) {
const action =
typeof params.action === "string" ? params.action : "unknown";
logVerbose(
`slack tool: action=${action} accountId=${String(
resolvedParams.accountId,
).trim()}`,
);
}
return await handleSlackAction(resolvedParams, cfg, {
currentChannelId: options?.currentChannelId,
currentThreadTs: options?.currentThreadTs,
replyToMode: options?.replyToMode,
hasRepliedRef: options?.hasRepliedRef,
});
},
};
}

View File

@@ -1,34 +0,0 @@
import { Type } from "@sinclair/typebox";
import { createReactionSchema } from "./reaction-schema.js";
// NOTE: chatId and messageId use Type.String() instead of Type.Union([Type.String(), Type.Number()])
// because nested anyOf schemas cause JSON Schema validation failures with Claude API on Vertex AI.
// Telegram IDs are coerced to strings at runtime in telegram-actions.ts.
export const TelegramToolSchema = Type.Union([
createReactionSchema({
ids: {
chatId: Type.String(),
messageId: Type.String(),
},
includeRemove: true,
}),
Type.Object({
action: Type.Literal("sendMessage"),
to: Type.String({ description: "Chat ID, @username, or t.me/username" }),
content: Type.String({ description: "Message text to send" }),
mediaUrl: Type.Optional(
Type.String({ description: "URL of image/video/audio to attach" }),
),
replyToMessageId: Type.Optional(
Type.Union([Type.String(), Type.Number()], {
description: "Message ID to reply to (for threading)",
}),
),
messageThreadId: Type.Optional(
Type.Union([Type.String(), Type.Number()], {
description: "Forum topic thread ID (for forum supergroups)",
}),
),
}),
]);

View File

@@ -1,18 +0,0 @@
import { loadConfig } from "../../config/config.js";
import type { AnyAgentTool } from "./common.js";
import { handleTelegramAction } from "./telegram-actions.js";
import { TelegramToolSchema } from "./telegram-schema.js";
export function createTelegramTool(): AnyAgentTool {
return {
label: "Telegram",
name: "telegram",
description: "Send messages and manage reactions on Telegram.",
parameters: TelegramToolSchema,
execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>;
const cfg = loadConfig();
return await handleTelegramAction(params, cfg);
},
};
}

View File

@@ -1,18 +0,0 @@
import { Type } from "@sinclair/typebox";
import { createReactionSchema } from "./reaction-schema.js";
export const WhatsAppToolSchema = Type.Union([
createReactionSchema({
ids: {
chatJid: Type.String(),
messageId: Type.String(),
},
includeRemove: true,
extras: {
participant: Type.Optional(Type.String()),
accountId: Type.Optional(Type.String()),
fromMe: Type.Optional(Type.Boolean()),
},
}),
]);

View File

@@ -1,18 +0,0 @@
import { loadConfig } from "../../config/config.js";
import type { AnyAgentTool } from "./common.js";
import { handleWhatsAppAction } from "./whatsapp-actions.js";
import { WhatsAppToolSchema } from "./whatsapp-schema.js";
export function createWhatsAppTool(): AnyAgentTool {
return {
label: "WhatsApp",
name: "whatsapp",
description: "Manage WhatsApp reactions.",
parameters: WhatsAppToolSchema,
execute: async (_toolCallId, args) => {
const params = args as Record<string, unknown>;
const cfg = loadConfig();
return await handleWhatsAppAction(params, cfg);
},
};
}