* wip * copy polugin files * wip type changes * refactor: improve Twitch plugin code quality and fix all tests - Extract client manager registry for centralized lifecycle management - Refactor to use early returns and reduce mutations - Fix status check logic for clientId detection - Add comprehensive test coverage for new modules - Remove tests for unimplemented features (index.test.ts, resolver.test.ts) - Fix mock setup issues in test suite (149 tests now passing) - Improve error handling with errorResponse helper in actions.ts - Normalize token handling to eliminate duplication Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * use accountId * delete md file * delte tsconfig * adjust log level * fix probe logic * format * fix monitor * code review fixes * format * no mutation * less mutation * chain debug log * await authProvider setup * use uuid * use spread * fix tests * update docs and remove bot channel fallback * more readme fixes * remove comments + fromat * fix tests * adjust access control logic * format * install * simplify config object * remove duplicate log tags + log received messages * update docs * update tests * format * strip markdown in monitor * remove strip markdown config, enabled by default * default requireMention to true * fix store path arg * fix multi account id + add unit test * fix multi account id + add unit test * make channel required and update docs * remove whisper functionality * remove duplicate connect log * update docs with convert twitch link * make twitch message processing non blocking * schema consistent casing * remove noisy ignore log * use coreLogger --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
174 lines
4.7 KiB
TypeScript
174 lines
4.7 KiB
TypeScript
/**
|
|
* Twitch message actions adapter.
|
|
*
|
|
* Handles tool-based actions for Twitch, such as sending messages.
|
|
*/
|
|
|
|
import { DEFAULT_ACCOUNT_ID, getAccountConfig } from "./config.js";
|
|
import { twitchOutbound } from "./outbound.js";
|
|
import type { ChannelMessageActionAdapter, ChannelMessageActionContext } from "./types.js";
|
|
|
|
/**
|
|
* Create a tool result with error content.
|
|
*/
|
|
function errorResponse(error: string) {
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: JSON.stringify({ ok: false, error }),
|
|
},
|
|
],
|
|
details: { ok: false },
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Read a string parameter from action arguments.
|
|
*
|
|
* @param args - Action arguments
|
|
* @param key - Parameter key
|
|
* @param options - Options for reading the parameter
|
|
* @returns The parameter value or undefined if not found
|
|
*/
|
|
function readStringParam(
|
|
args: Record<string, unknown>,
|
|
key: string,
|
|
options: { required?: boolean; trim?: boolean } = {},
|
|
): string | undefined {
|
|
const value = args[key];
|
|
if (value === undefined || value === null) {
|
|
if (options.required) {
|
|
throw new Error(`Missing required parameter: ${key}`);
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// Convert value to string safely
|
|
if (typeof value === "string") {
|
|
return options.trim !== false ? value.trim() : value;
|
|
}
|
|
|
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
const str = String(value);
|
|
return options.trim !== false ? str.trim() : str;
|
|
}
|
|
|
|
throw new Error(`Parameter ${key} must be a string, number, or boolean`);
|
|
}
|
|
|
|
/** Supported Twitch actions */
|
|
const TWITCH_ACTIONS = new Set(["send" as const]);
|
|
type TwitchAction = typeof TWITCH_ACTIONS extends Set<infer U> ? U : never;
|
|
|
|
/**
|
|
* Twitch message actions adapter.
|
|
*/
|
|
export const twitchMessageActions: ChannelMessageActionAdapter = {
|
|
/**
|
|
* List available actions for this channel.
|
|
*/
|
|
listActions: () => [...TWITCH_ACTIONS],
|
|
|
|
/**
|
|
* Check if an action is supported.
|
|
*/
|
|
supportsAction: ({ action }) => TWITCH_ACTIONS.has(action as TwitchAction),
|
|
|
|
/**
|
|
* Extract tool send parameters from action arguments.
|
|
*
|
|
* Parses and validates the "to" and "message" parameters for sending.
|
|
*
|
|
* @param params - Arguments from the tool call
|
|
* @returns Parsed send parameters or null if invalid
|
|
*
|
|
* @example
|
|
* const result = twitchMessageActions.extractToolSend!({
|
|
* args: { to: "#mychannel", message: "Hello!" }
|
|
* });
|
|
* // Returns: { to: "#mychannel", message: "Hello!" }
|
|
*/
|
|
extractToolSend: ({ args }) => {
|
|
try {
|
|
const to = readStringParam(args, "to", { required: true });
|
|
const message = readStringParam(args, "message", { required: true });
|
|
|
|
if (!to || !message) {
|
|
return null;
|
|
}
|
|
|
|
return { to, message };
|
|
} catch {
|
|
return null;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Handle an action execution.
|
|
*
|
|
* Processes the "send" action to send messages to Twitch.
|
|
*
|
|
* @param ctx - Action context including action type, parameters, and config
|
|
* @returns Tool result with content or null if action not supported
|
|
*
|
|
* @example
|
|
* const result = await twitchMessageActions.handleAction!({
|
|
* action: "send",
|
|
* params: { message: "Hello Twitch!", to: "#mychannel" },
|
|
* cfg: clawdbotConfig,
|
|
* accountId: "default",
|
|
* });
|
|
*/
|
|
handleAction: async (
|
|
ctx: ChannelMessageActionContext,
|
|
): Promise<{ content: Array<{ type: string; text: string }> } | null> => {
|
|
if (ctx.action !== "send") {
|
|
return null;
|
|
}
|
|
|
|
const message = readStringParam(ctx.params, "message", { required: true });
|
|
const to = readStringParam(ctx.params, "to", { required: false });
|
|
const accountId = ctx.accountId ?? DEFAULT_ACCOUNT_ID;
|
|
|
|
const account = getAccountConfig(ctx.cfg, accountId);
|
|
if (!account) {
|
|
return errorResponse(
|
|
`Account not found: ${accountId}. Available accounts: ${Object.keys(ctx.cfg.channels?.twitch?.accounts ?? {}).join(", ") || "none"}`,
|
|
);
|
|
}
|
|
|
|
// Use the channel from account config (or override with `to` parameter)
|
|
const targetChannel = to || account.channel;
|
|
if (!targetChannel) {
|
|
return errorResponse("No channel specified and no default channel in account config");
|
|
}
|
|
|
|
if (!twitchOutbound.sendText) {
|
|
return errorResponse("sendText not implemented");
|
|
}
|
|
|
|
try {
|
|
const result = await twitchOutbound.sendText({
|
|
cfg: ctx.cfg,
|
|
to: targetChannel,
|
|
text: message ?? "",
|
|
accountId,
|
|
});
|
|
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: JSON.stringify(result),
|
|
},
|
|
],
|
|
details: { ok: true },
|
|
};
|
|
} catch (error) {
|
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
return errorResponse(errorMsg);
|
|
}
|
|
},
|
|
};
|