agent-orchestrator/packages/plugins/notifier-slack/src/index.ts

189 lines
5.0 KiB
TypeScript

import {
validateUrl,
type PluginModule,
type Notifier,
type OrchestratorEvent,
type NotifyAction,
type NotifyContext,
type EventPriority,
CI_STATUS,
} from "@composio/ao-core";
export const manifest = {
name: "slack",
slot: "notifier" as const,
description: "Notifier plugin: Slack webhook notifications",
version: "0.1.0",
};
const PRIORITY_EMOJI: Record<EventPriority, string> = {
urgent: ":rotating_light:",
action: ":point_right:",
warning: ":warning:",
info: ":information_source:",
};
function buildBlocks(event: OrchestratorEvent, actions?: NotifyAction[]): unknown[] {
const blocks: unknown[] = [
{
type: "header",
text: {
type: "plain_text",
text: `${PRIORITY_EMOJI[event.priority]} ${event.type}${event.sessionId}`,
emoji: true,
},
},
{
type: "section",
text: {
type: "mrkdwn",
text: event.message,
},
},
{
type: "context",
elements: [
{
type: "mrkdwn",
text: `*Project:* ${event.projectId} | *Priority:* ${event.priority} | *Time:* <!date^${Math.floor(event.timestamp.getTime() / 1000)}^{date_short_pretty} {time}|${event.timestamp.toISOString()}>`,
},
],
},
];
// Add PR link if available (type-guarded)
const prUrl = typeof event.data.prUrl === "string" ? event.data.prUrl : undefined;
if (prUrl) {
blocks.push({
type: "section",
text: {
type: "mrkdwn",
text: `:github: <${prUrl}|View Pull Request>`,
},
});
}
// Add CI status if available (type-guarded)
const ciStatus = typeof event.data.ciStatus === "string" ? event.data.ciStatus : undefined;
if (ciStatus) {
const ciEmoji = ciStatus === CI_STATUS.PASSING ? ":white_check_mark:" : ":x:";
blocks.push({
type: "context",
elements: [
{
type: "mrkdwn",
text: `${ciEmoji} CI: ${ciStatus}`,
},
],
});
}
// Add action buttons
if (actions && actions.length > 0) {
const elements = actions
.filter((a) => a.url || a.callbackEndpoint)
.map((action) => {
if (action.url) {
return {
type: "button",
text: { type: "plain_text", text: action.label, emoji: true },
url: action.url,
};
}
const sanitized = action.label
.toLowerCase()
.replace(/[^a-z0-9]+/g, "_")
.replace(/^_|_$/g, "");
const idx = actions.indexOf(action);
const actionId = sanitized ? `${sanitized}_${idx}` : `action_${idx}`;
return {
type: "button",
text: { type: "plain_text", text: action.label, emoji: true },
action_id: `ao_${actionId}`,
value: action.callbackEndpoint,
};
});
if (elements.length > 0) {
blocks.push({
type: "actions",
elements,
});
}
}
blocks.push({ type: "divider" });
return blocks;
}
async function postToWebhook(webhookUrl: string, payload: Record<string, unknown>): Promise<void> {
const response = await fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
const body = await response.text();
throw new Error(`Slack webhook failed (${response.status}): ${body}`);
}
}
export function create(config?: Record<string, unknown>): Notifier {
const webhookUrl = config?.webhookUrl as string | undefined;
const defaultChannel = config?.channel as string | undefined;
const username = (config?.username as string) ?? "Agent Orchestrator";
if (!webhookUrl) {
console.warn("[notifier-slack] No webhookUrl configured — notifications will be no-ops");
} else {
validateUrl(webhookUrl, "notifier-slack");
}
return {
name: "slack",
async notify(event: OrchestratorEvent): Promise<void> {
if (!webhookUrl) return;
const payload: Record<string, unknown> = {
username,
blocks: buildBlocks(event),
};
if (defaultChannel) payload.channel = defaultChannel;
await postToWebhook(webhookUrl, payload);
},
async notifyWithActions(event: OrchestratorEvent, actions: NotifyAction[]): Promise<void> {
if (!webhookUrl) return;
const payload: Record<string, unknown> = {
username,
blocks: buildBlocks(event, actions),
};
if (defaultChannel) payload.channel = defaultChannel;
await postToWebhook(webhookUrl, payload);
},
async post(message: string, context?: NotifyContext): Promise<string | null> {
if (!webhookUrl) return null;
const channel = context?.channel ?? defaultChannel;
const payload: Record<string, unknown> = {
username,
text: message,
};
if (channel) payload.channel = channel;
await postToWebhook(webhookUrl, payload);
// Incoming webhooks don't return a message ID
return null;
},
};
}
export default { manifest, create } satisfies PluginModule<Notifier>;