@ararahq/mcp 5.0.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,38 @@
1
+ import { percentOf } from "./format.js";
2
+ const STEPS = [
3
+ ["sentCount", "sent", "Enviadas"],
4
+ ["deliveredCount", "delivered", "Entregues"],
5
+ ["readCount", "read", "Lidas"],
6
+ ["clickedCount", "clicked", "Clicaram"],
7
+ ["replyCount", "replied", "Responderam"],
8
+ ["convertedCount", "converted", "Converteram"],
9
+ ];
10
+ /** Funnel rows in order, each as count and percent of the audience. */
11
+ export const buildFunnel = (counts) => STEPS.flatMap(([field, key, label]) => {
12
+ const count = counts[field];
13
+ if (typeof count !== "number")
14
+ return [];
15
+ return [{ key, label, count, percent: percentOf(count, counts.totalMessages) }];
16
+ });
17
+ export const TERMINAL_STATUSES = new Set([
18
+ "COMPLETED",
19
+ "COMPLETED_WITH_ERRORS",
20
+ "CANCELED",
21
+ "FAILED",
22
+ ]);
23
+ export const isCampaignRunning = (status) => !TERMINAL_STATUSES.has(status.toUpperCase());
24
+ export const humanizeCampaignStatus = (status) => {
25
+ const map = {
26
+ INGESTING: "Preparando",
27
+ SCHEDULED: "Agendada",
28
+ SENDING: "Disparando",
29
+ AB_TESTING: "Teste A/B",
30
+ COMPLETED: "Concluída",
31
+ COMPLETED_WITH_ERRORS: "Concluída com falhas",
32
+ CANCELED: "Cancelada",
33
+ FAILED: "Falhou",
34
+ };
35
+ return map[status.toUpperCase()] ?? status;
36
+ };
37
+ /** Share of the audience already sent, used for the live progress ring. */
38
+ export const progressPercent = (counts) => percentOf(counts.sentCount + (counts.failedCount ?? 0), counts.totalMessages);
@@ -0,0 +1,21 @@
1
+ const isRecord = (value) => typeof value === "object" && value !== null;
2
+ /** Reads the MCP structured content of any Arara tool into a typed envelope. */
3
+ export const readEnvelope = (structuredContent) => {
4
+ if (!isRecord(structuredContent)) {
5
+ return {
6
+ ok: false,
7
+ error: { code: "EMPTY_RESULT", message: "Sem dados na resposta.", retryable: false },
8
+ };
9
+ }
10
+ if (structuredContent.ok === true)
11
+ return { ok: true, data: structuredContent.data };
12
+ const error = isRecord(structuredContent.error) ? structuredContent.error : {};
13
+ return {
14
+ ok: false,
15
+ error: {
16
+ code: typeof error.code === "string" ? error.code : "UNKNOWN",
17
+ message: typeof error.message === "string" ? error.message : "Algo deu errado.",
18
+ retryable: error.retryable === true,
19
+ },
20
+ };
21
+ };
@@ -0,0 +1,27 @@
1
+ const BRL = new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" });
2
+ const INTEGER = new Intl.NumberFormat("pt-BR");
3
+ const PERCENT_SCALE = 100;
4
+ export const formatBrl = (value) => BRL.format(Number.isFinite(value) ? value : 0);
5
+ export const formatInteger = (value) => INTEGER.format(Number.isFinite(value) ? Math.round(value) : 0);
6
+ export const percentOf = (count, total) => total > 0 ? Math.round((count / total) * PERCENT_SCALE) : 0;
7
+ export const formatTime = (iso) => {
8
+ if (typeof iso !== "string")
9
+ return "";
10
+ const date = new Date(iso);
11
+ if (Number.isNaN(date.getTime()))
12
+ return "";
13
+ return date.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
14
+ };
15
+ export const formatDate = (iso) => {
16
+ if (typeof iso !== "string")
17
+ return "";
18
+ const date = new Date(iso);
19
+ if (Number.isNaN(date.getTime()))
20
+ return "";
21
+ return date.toLocaleDateString("pt-BR", { day: "2-digit", month: "short" });
22
+ };
23
+ /** Replaces {{1}}, {{2}}... with positional values, leaving unknown slots visible. */
24
+ export const renderTemplateBody = (body, variables) => body.replace(/\{\{(\d+)\}\}/g, (match, index) => {
25
+ const value = variables[Number.parseInt(index, 10) - 1];
26
+ return typeof value === "string" && value.length > 0 ? value : match;
27
+ });
@@ -0,0 +1,48 @@
1
+ const ORDER = ["accepted", "sent", "delivered", "read"];
2
+ const LABELS = {
3
+ accepted: "Aceita",
4
+ sent: "Enviada",
5
+ delivered: "Entregue",
6
+ read: "Lida",
7
+ };
8
+ const STATUS_RANK = {
9
+ PENDING: 0,
10
+ QUEUED: 0,
11
+ ACCEPTED: 0,
12
+ SENT: 1,
13
+ SENT_TO_PROVIDER: 1,
14
+ ENVIADA: 1,
15
+ DELIVERED: 2,
16
+ ENTREGUE: 2,
17
+ READ: 3,
18
+ LIDA: 3,
19
+ };
20
+ export const isFailedStatus = (status) => /FAIL|ERRO|REJECT|UNDELIVER/i.test(status);
21
+ export const isTerminalStatus = (status) => isFailedStatus(status) || STATUS_RANK[status.toUpperCase()] === 3;
22
+ /** Maps a provider status to the four visual steps of a delivery timeline. */
23
+ export const buildTimeline = (status) => {
24
+ const normalized = status.toUpperCase();
25
+ const failed = isFailedStatus(normalized);
26
+ const rank = STATUS_RANK[normalized] ?? 0;
27
+ return ORDER.map((key, index) => {
28
+ if (failed)
29
+ return {
30
+ key,
31
+ label: LABELS[key],
32
+ state: index === 0 ? "done" : index === 1 ? "failed" : "pending",
33
+ };
34
+ if (index < rank)
35
+ return { key, label: LABELS[key], state: "done" };
36
+ if (index === rank)
37
+ return { key, label: LABELS[key], state: rank === 3 ? "done" : "current" };
38
+ return { key, label: LABELS[key], state: "pending" };
39
+ });
40
+ };
41
+ export const humanizeMessageStatus = (status) => {
42
+ if (isFailedStatus(status))
43
+ return "Falhou";
44
+ const rank = STATUS_RANK[status.toUpperCase()];
45
+ if (rank === undefined)
46
+ return status;
47
+ return LABELS[ORDER[rank] ?? "accepted"];
48
+ };
@@ -0,0 +1,25 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
4
+ export const UI_PANELS = {
5
+ campaign: "Campaign report with funnel, live progress and next actions.",
6
+ broadcast: "Broadcast preview with phone mockup, approval and live delivery.",
7
+ status: "Delivery timeline, 24h window state or template approval.",
8
+ conversation: "Chat thread with one person and an inline reply box.",
9
+ };
10
+ export const uiResourceUri = (panel) => `ui://arara/${panel}.html`;
11
+ const FALLBACK_HTML = "<!doctype html><p>Panel not built. Run npm run build.</p>";
12
+ export const loadPanelHtml = async (panel, dir = import.meta.dirname) => readFile(path.join(dir, `${panel}.html`), "utf8").catch(() => FALLBACK_HTML);
13
+ export const registerUiResources = (server) => {
14
+ for (const [panel, description] of Object.entries(UI_PANELS)) {
15
+ const uri = uiResourceUri(panel);
16
+ server.registerResource(`ui_${panel}`, uri, {
17
+ title: `Arara ${panel} panel`,
18
+ description,
19
+ mimeType: RESOURCE_MIME_TYPE,
20
+ _meta: { ui: { prefersBorder: true } },
21
+ }, async () => ({
22
+ contents: [{ uri, mimeType: RESOURCE_MIME_TYPE, text: await loadPanelHtml(panel) }],
23
+ }));
24
+ }
25
+ };