conduyt-mcp 4.93.0 → 4.95.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.
- package/README.md +12 -2
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -1
- package/dist/tools/chat.js +79 -5
- package/dist/tools/workflow-sandbox.js +16 -2
- package/package.json +1 -1
- package/scripts/coverage-matrix.json +48 -6
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ MCP server for [Conduyt CRM](https://conduyt.app) — expose your CRM as AI-acce
|
|
|
88
88
|
| Tool | Description |
|
|
89
89
|
|------|-------------|
|
|
90
90
|
| `conduyt_workflow_validate` | Validate a workflow graph without saving — returns errors and warnings |
|
|
91
|
-
| `conduyt_workflow_simulate` | Dry-run a workflow against a real or stub contact — step-by-step trace with compliance and provider checks |
|
|
91
|
+
| `conduyt_workflow_simulate` | Dry-run a workflow against a real or stub contact — step-by-step trace with compliance and provider checks; optional `dealId` (deal occurrence), `companyId` (company occurrence) and `executionId` (replays a stored run, admin or owner only, supplies the company its payload carried) |
|
|
92
92
|
| `conduyt_workflow_promote` | Safely publish a workflow — validates first, refuses if errors exist |
|
|
93
93
|
|
|
94
94
|
### Sequences (Email and SMS)
|
|
@@ -239,8 +239,18 @@ The Mailchimp OAuth connect and callback are browser-only: connect Mailchimp in
|
|
|
239
239
|
| `conduyt_get_chat_unread` | `GET /chat/unread`: per channel `{ channelId, unread, soundUnread, newestSeq, latestSeq }`; may list an auto channel you have not opened yet (no membership row) |
|
|
240
240
|
| `conduyt_list_chat_messages` | List a channel's messages, newest first; every message carries `seq` (commit-ordered position in the channel); does not move the read marker; on a 403 "Not a member of this channel" it calls `GET /chat/channels` once and retries once |
|
|
241
241
|
| `conduyt_mark_chat_read` | Acknowledge read up to one message (`readUpTo` = message UUID; the marker only moves forward, an older or foreign message is a no-op; same one-retry membership bootstrap) |
|
|
242
|
+
| `conduyt_get_chat_realtime_token` | `GET /realtime/token`: a signed Ably token request for the realtime pipe (`channelIds` up to 10, sent as repeated `channel=`); `{ enabled: false, reason }` when the account switch is off; returns `accountId` / `userId` |
|
|
243
|
+
| `conduyt_get_chat_notifications` | `GET /chat/channels/{id}/notifications`: your level `{ level, isDefault, notify, updatedAt, membershipId, membershipSince, membershipSeq }` |
|
|
244
|
+
| `conduyt_set_chat_notifications` | `PATCH /chat/channels/{id}/notifications`: compare-and-set (`expectedUpdatedAt` or null, `expectedMembershipId`, `accountId`, `userId`); a 409 `stale` comes back as an error result carrying the current membership, never retried; 404 `not_a_member` and 403 `identity_mismatch` surface as is |
|
|
245
|
+
| `conduyt_update_team_chat_settings` | `PATCH /settings` `teamChat` (owner/admin only): `realtime`, `realtimePresence`, `quietHours` (`{start,end}` HH:MM or null), `digestMinutes` (0 or 15-240) |
|
|
242
246
|
|
|
243
|
-
|
|
247
|
+
`conduyt_list_chat_messages` also takes `afterSeq` (messages committed after that seq; once, a whole number, never with `cursor`) and every message carries `version` (a counter bumped on each update). `conduyt_get_chat_unread` rows carry `mentions`, `notify`, `updatedAt`, `membershipId` (null = no membership), `membershipSince`, `membershipSeq`, and the response `teamChatRealtime` and `membershipSeqHigh`; `conduyt_list_chat_channels` rows carry `mentionCount` and the same membership fields.
|
|
248
|
+
|
|
249
|
+
The rest of team chat (sending, edit/delete, reactions, pins, claim/handoff, typing, presence, uploads) is not wrapped yet. `GET /chat/messages/{messageId}/location` is a UI deep-link helper and deliberately not wrapped. `GET /cron/team-chat-digest` is a CRON_SECRET-gated job and not key-callable.
|
|
250
|
+
|
|
251
|
+
#### What the API publishes (realtime events, not tools)
|
|
252
|
+
|
|
253
|
+
Team Chat 2.0 publishes thin events (ids and kinds, never message content) on the Ably channels the token above authorizes; this server only fetches the token, it does not subscribe. Chat channel `acct:<account>:chat:<channel>`, event `chat`: `created { channelId, id, seq }`, `updated { channelId, id, what: edited|deleted|reaction|pin|claim|handoff }`, `typing { channelId, userId, name }`. User channel `acct:<account>:user:<user>`, event `user`: `message { channelId, id, seq, dm }`, `read { channelId, seq }`, `chat.membership.notify { channelId, notify, updatedAt, membershipId, membershipSince, membershipSeq }`. Presence channel `acct:<account>:presence` (only with `realtimePresence` on). Everything is behind the account flag `settings.teamChat.realtime`.
|
|
244
254
|
|
|
245
255
|
### Custom Fields
|
|
246
256
|
| Tool | Description |
|
package/dist/client.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ export interface RequestOptions {
|
|
|
30
30
|
maxResponseBytes?: number;
|
|
31
31
|
/** Extra request headers (e.g. Idempotency-Key on POST /tasks); never overrides Authorization / Content-Type. */
|
|
32
32
|
headers?: Record<string, string>;
|
|
33
|
+
/** Send a GET exactly once: a 429/5xx (or a timeout) is the caller's first refusal, never retried or replaced by a later answer. */
|
|
34
|
+
noRetry?: boolean;
|
|
33
35
|
}
|
|
34
36
|
/**
|
|
35
37
|
* Render an error body's details (everything but `error`) as ONE valid JSON object: scalars first (code, revision; a very
|
package/dist/client.js
CHANGED
|
@@ -341,7 +341,7 @@ export class ConduytClient {
|
|
|
341
341
|
async requestRaw(method, path, body, opts) {
|
|
342
342
|
const timeoutMs = opts?.timeoutMs ?? this.timeoutMs;
|
|
343
343
|
const maxResponseBytes = opts?.maxResponseBytes ?? this.maxResponseBytes;
|
|
344
|
-
const canRetry = method === "GET";
|
|
344
|
+
const canRetry = method === "GET" && !opts?.noRetry;
|
|
345
345
|
const attempts = canRetry ? MAX_GET_RETRIES + 1 : 1;
|
|
346
346
|
// TOTAL time budget across every attempt + backoff: 1.5x the per-call
|
|
347
347
|
// deadline. Without it, 3 stacked attempt timeouts + backoffs could hold
|
package/dist/tools/chat.js
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { ConduytApiError, formatResult } from "../client.js";
|
|
2
|
+
import { ConduytApiError, formatResult, formatToolError } from "../client.js";
|
|
3
|
+
import { patchGeneralSettings } from "./lifecycle.js";
|
|
3
4
|
import { strictInput } from "./strict.js";
|
|
5
|
+
/** The error text's details are ONE JSON object and the final token (client.ts error grammar): try each `{` from the left. */
|
|
6
|
+
function errorDetailsOf(message) {
|
|
7
|
+
for (let i = message.indexOf("{"); i !== -1; i = message.indexOf("{", i + 1)) {
|
|
8
|
+
try {
|
|
9
|
+
const v = JSON.parse(message.slice(i));
|
|
10
|
+
if (v && typeof v === "object" && !Array.isArray(v))
|
|
11
|
+
return v;
|
|
12
|
+
}
|
|
13
|
+
catch { /* not the details object: try the next brace */ }
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
/** the 409 stale answer: the API's body (code, error, current membership) as a structured error result, never retried */
|
|
18
|
+
function formatToolError409(message, details) {
|
|
19
|
+
const base = formatToolError(message);
|
|
20
|
+
return { ...base, content: [{ type: "text", text: JSON.stringify({ ok: false, status: 409, code: details.code ?? "stale", error: typeof details.error === "string" ? details.error : "stale", current: details.current ?? null, message }, null, 2) }] };
|
|
21
|
+
}
|
|
22
|
+
const HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
|
|
4
23
|
/** The messages route's membership refusal (403 "Not a member of this channel"); any other 403 (a permission) is NOT this. */
|
|
5
24
|
export function isMembershipRefusal(err) {
|
|
6
25
|
return err instanceof Error && /\b403\b/.test(err.message) && /Not a member of this channel/.test(err.message)
|
|
@@ -24,24 +43,29 @@ async function withMembershipBootstrap(client, read) {
|
|
|
24
43
|
}
|
|
25
44
|
/** Team chat read side (app 10-09, live sha 3869c79ba): the unread vector, messages carrying `seq`, and the read acknowledgement. */
|
|
26
45
|
export function registerChatTools(server, client) {
|
|
27
|
-
strictInput(server.tool("conduyt_list_chat_channels", "List the team chat channels the API key's user belongs to (GET /api/v1/chat/channels): id, name, kind, members, last message, counts. SIDE EFFECT: it also creates the caller's memberships in the account's AUTO channels (needs-a-hand, new-leads, wins) the first time, so a user who never opened Team Chat can read them. conduyt_list_chat_messages and conduyt_mark_chat_read already do this once for you when a channel listed by conduyt_get_chat_unread answers 403 'Not a member of this channel'; call this tool directly to discover channel ids.", {}, async () => {
|
|
46
|
+
strictInput(server.tool("conduyt_list_chat_channels", "List the team chat channels the API key's user belongs to (GET /api/v1/chat/channels): id, name, kind, members, last message, counts. Each row also carries the Team Chat 2.0 fields mentionCount (unread @mentions of you), notify (your stored notification level: all, mentions, mute, or null = the kind's default), updatedAt (when notify last changed, null = never), membershipId (your member row; changes after a removal and re-add), membershipSince (when this membership began) and membershipSeq (strictly increasing incarnation number). SIDE EFFECT: it also creates the caller's memberships in the account's AUTO channels (needs-a-hand, new-leads, wins) the first time, so a user who never opened Team Chat can read them. conduyt_list_chat_messages and conduyt_mark_chat_read already do this once for you when a channel listed by conduyt_get_chat_unread answers 403 'Not a member of this channel'; call this tool directly to discover channel ids.", {}, async () => {
|
|
28
47
|
const result = await client.get("/api/v1/chat/channels");
|
|
29
48
|
return formatResult(result);
|
|
30
49
|
}));
|
|
31
|
-
strictInput(server.tool("conduyt_get_chat_unread", "Team chat unread vector for the API key's user (GET /api/v1/chat/unread): one row per channel the user is a member of, { channelId, unread, soundUnread, newestSeq, latestSeq }. unread is the badge count (auto channels are 0 by design); soundUnread counts unread messages in every channel, auto ones included; newestSeq is the seq of the channel's newest message not written by the user (null = none, panic cards left out); latestSeq is the same including panic cards. Read-only: no writes, never creates a membership, so a row can be an AUTO channel the caller has not opened yet (no membership row: reading it first answers 403, which conduyt_list_chat_messages / conduyt_mark_chat_read recover from by calling conduyt_list_chat_channels once and retrying once). Use it to decide which channel to read next, then conduyt_list_chat_messages.", {}, async () => {
|
|
50
|
+
strictInput(server.tool("conduyt_get_chat_unread", "Team chat unread vector for the API key's user (GET /api/v1/chat/unread): one row per channel the user is a member of, { channelId, unread, soundUnread, newestSeq, latestSeq }. unread is the badge count (auto channels are 0 by design); soundUnread counts unread messages in every channel, auto ones included; newestSeq is the seq of the channel's newest message not written by the user (null = none, panic cards left out); latestSeq is the same including panic cards. Team Chat 2.0 (account flag settings.teamChat.realtime) adds per row: mentions (unread @mentions of you), notify (your stored notification level: all, mentions, mute, or null = default), updatedAt (when notify last changed, null = never), membershipId (null = no membership row), membershipSince and membershipSeq; and per response: teamChatRealtime (whether the account's realtime switch is on) and membershipSeqHigh (the highest membershipSeq among your rows). Read-only: no writes, never creates a membership, so a row can be an AUTO channel the caller has not opened yet (no membership row: reading it first answers 403, which conduyt_list_chat_messages / conduyt_mark_chat_read recover from by calling conduyt_list_chat_channels once and retrying once). Use it to decide which channel to read next, then conduyt_list_chat_messages.", {}, async () => {
|
|
32
51
|
const result = await client.get("/api/v1/chat/unread");
|
|
33
52
|
return formatResult(result);
|
|
34
53
|
}));
|
|
35
|
-
strictInput(server.tool("conduyt_list_chat_messages", "List messages in a team chat channel (GET /api/v1/chat/channels/{id}/messages), newest first, paged by cursor. The caller must be a channel member (else 403). Every message carries seq, its commit-ordered position inside the channel (a total order: the list and the cursor follow it). Read-only: this tool does not move the read marker. On a 403 'Not a member of this channel' it calls GET /chat/channels once (which creates the caller's auto-channel memberships) and retries ONCE; a second refusal, or a 403 for any other reason, is returned as is; acknowledge with conduyt_mark_chat_read.", {
|
|
54
|
+
strictInput(server.tool("conduyt_list_chat_messages", "List messages in a team chat channel (GET /api/v1/chat/channels/{id}/messages), newest first, paged by cursor. The caller must be a channel member (else 403). Every message carries seq, its commit-ordered position inside the channel (a total order: the list and the cursor follow it), and version, a server counter bumped on every update (edit, delete, reaction, pin, claim, handoff) so a client can tell which copy of a message is newer. afterSeq=N is the realtime catch-up read: the messages committed AFTER seq N (give it once, a whole number 0-999999999, never together with cursor: both are refused here before any request); the page is still newest first and hasMore says more follow it (then re-read the latest page). Read-only: this tool does not move the read marker. On a 403 'Not a member of this channel' it calls GET /chat/channels once (which creates the caller's auto-channel memberships) and retries ONCE; a second refusal, or a 403 for any other reason, is returned as is; acknowledge with conduyt_mark_chat_read.", {
|
|
36
55
|
channelId: z.string().uuid().describe("Channel UUID"),
|
|
37
56
|
limit: z.number().int().min(1).max(100).optional().describe("Messages per page (1-100, default 50)"),
|
|
38
57
|
cursor: z.string().uuid().optional().describe("nextCursor from the previous page (a message UUID)"),
|
|
39
|
-
|
|
58
|
+
afterSeq: z.number().int().min(0).max(999_999_999).optional().describe("Only messages committed after this seq (the realtime catch-up read). Not combinable with cursor"),
|
|
59
|
+
}, async ({ channelId, limit, cursor, afterSeq }) => {
|
|
60
|
+
if (afterSeq !== undefined && cursor !== undefined)
|
|
61
|
+
throw new Error("afterSeq and cursor cannot be combined");
|
|
40
62
|
const params = new URLSearchParams();
|
|
41
63
|
if (limit !== undefined)
|
|
42
64
|
params.set("limit", String(limit));
|
|
43
65
|
if (cursor !== undefined)
|
|
44
66
|
params.set("cursor", cursor);
|
|
67
|
+
if (afterSeq !== undefined)
|
|
68
|
+
params.set("afterSeq", String(afterSeq));
|
|
45
69
|
const result = await withMembershipBootstrap(client, () => client.get(`/api/v1/chat/channels/${channelId}/messages?${params}`));
|
|
46
70
|
return formatResult(result);
|
|
47
71
|
}));
|
|
@@ -52,4 +76,54 @@ export function registerChatTools(server, client) {
|
|
|
52
76
|
const result = await withMembershipBootstrap(client, () => client.get(`/api/v1/chat/channels/${channelId}/messages?limit=1&readUpTo=${readUpTo}`));
|
|
53
77
|
return formatResult(result);
|
|
54
78
|
}));
|
|
79
|
+
strictInput(server.tool("conduyt_get_chat_realtime_token", "Team Chat 2.0 realtime token (GET /api/v1/realtime/token): a signed Ably token request for the account's realtime pipe, scoped to your own user channel, the account presence channel (only when settings.teamChat.realtimePresence is on) and every chat channel you are a member of (the 100 most recently active, plus the ones you name). Answers { enabled: true, tokenRequest, accountId, userId, channels, presence, expiresAt } or { enabled: false, reason: 'off' | 'unconfigured' } when the account's realtime switch (settings.teamChat.realtime) is off or the provider is not configured: stay on polling then. accountId and userId are the values conduyt_set_chat_notifications needs. channelIds (optional, up to 10 UUIDs) are sent as repeated channel=<uuid> parameters; a malformed id or more than 10 is refused 400 here or by the API; an inactive or unaccepted member gets 403 'Not an active member of this account'; 429 'Too many token requests' (30 a minute) is surfaced from the FIRST request: this read is sent once and never retried. Read-only; the token is short-lived and never to be logged.", {
|
|
80
|
+
channelIds: z.array(z.string().uuid()).max(10).optional().describe("Channels to include in the capability (each one you are a member of), up to 10"),
|
|
81
|
+
}, async ({ channelIds }) => {
|
|
82
|
+
const params = new URLSearchParams();
|
|
83
|
+
for (const id of channelIds ?? [])
|
|
84
|
+
params.append("channel", id);
|
|
85
|
+
const result = await client.get(`/api/v1/realtime/token?${params}`, { noRetry: true });
|
|
86
|
+
return formatResult(result);
|
|
87
|
+
}));
|
|
88
|
+
strictInput(server.tool("conduyt_get_chat_notifications", "Your notification level for one team chat channel (GET /api/v1/chat/channels/{id}/notifications): { level, isDefault, notify, updatedAt, membershipId, membershipSince, membershipSeq }. level is the effective level (all = every message pushes, mentions = only @mentions of you, mute = nothing but a direct @mention); notify is the STORED value (null = the kind's default: a DM is followed, a channel is mentions-only); updatedAt is the version of that value (null = never set) and membershipId the member row; pass both to conduyt_set_chat_notifications. Members only (else 403 'Not a member of this channel', recovered once like the other reads by calling GET /chat/channels and retrying once). Read-only.", { channelId: z.string().uuid().describe("Channel UUID") }, async ({ channelId }) => {
|
|
89
|
+
const result = await withMembershipBootstrap(client, () => client.get(`/api/v1/chat/channels/${channelId}/notifications`));
|
|
90
|
+
return formatResult(result);
|
|
91
|
+
}));
|
|
92
|
+
strictInput(server.tool("conduyt_set_chat_notifications", "Set your notification level for one team chat channel (PATCH /api/v1/chat/channels/{id}/notifications). The write is compare-and-set against what you last READ: read conduyt_get_chat_notifications first and send its updatedAt as expectedUpdatedAt (null = never set) and its membershipId as expectedMembershipId; accountId and userId are the person you mean (from conduyt_get_chat_realtime_token), and a key whose user is someone else is refused 403 identity_mismatch. level: all, mentions, mute, or default (back to the kind's default). Success returns the same fields as the read. A stale write is NOT applied and NOT retried: the tool answers an error result whose body is { ok: false, status: 409, code: 'stale', error, current: { notify, updatedAt, membershipId, membershipSince, membershipSeq } } with the CURRENT membership, so you re-decide from it (a different membershipId means you were removed and re-added). 404 not_a_member (no longer a member) and 403 identity_mismatch / 'Not a member of this channel' are surfaced as is; 400 for a bad level, expectedUpdatedAt or expectedMembershipId. Takes effect only while the account's realtime switch (settings.teamChat.realtime) is on.", {
|
|
93
|
+
channelId: z.string().uuid().describe("Channel UUID"),
|
|
94
|
+
level: z.enum(["all", "mentions", "mute", "default"]).describe("all, mentions, mute, or default (the kind's default)"),
|
|
95
|
+
expectedUpdatedAt: z.string().datetime({ offset: true }).nullable().describe("The updatedAt you last read (ISO date), or null when it was null (never set)"),
|
|
96
|
+
expectedMembershipId: z.string().uuid().describe("The membershipId you last read"),
|
|
97
|
+
accountId: z.string().uuid().describe("Your account id (conduyt_get_chat_realtime_token returns it)"),
|
|
98
|
+
userId: z.string().uuid().describe("Your user id (conduyt_get_chat_realtime_token returns it)"),
|
|
99
|
+
}, async ({ channelId, level, expectedUpdatedAt, expectedMembershipId, accountId, userId }) => {
|
|
100
|
+
try {
|
|
101
|
+
const result = await client.patch(`/api/v1/chat/channels/${channelId}/notifications`, { level, expectedUpdatedAt, expectedMembershipId, accountId: accountId.toLowerCase(), userId: userId.toLowerCase() });
|
|
102
|
+
return formatResult(result);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
if (err instanceof ConduytApiError && err.status === 409) {
|
|
106
|
+
const details = errorDetailsOf(err.message) ?? {};
|
|
107
|
+
return formatToolError409(err.message, details);
|
|
108
|
+
}
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
strictInput(server.tool("conduyt_update_team_chat_settings", "Change the account's Team Chat 2.0 settings (PATCH /api/v1/settings, settings.teamChat). OWNER / ADMIN ONLY (else 403 forbidden 'Only an owner or admin can change Team Chat realtime delivery') and settings:edit. Pass only what changes (at least one): realtime (instant delivery of messages, edits, reactions, pins, typing, read receipts and unread signals over the realtime pipe; off = the chat polls; the account default is off, on for the QA tenant), realtimePresence (online/away over the pipe; needs realtime on; default off), quietHours ({ start, end } as HH:MM in the account timezone, end may be earlier than start to cross midnight; null clears; needs realtime on: no pushes and no digests in the window), digestMinutes (the missed-message email digest window: 0 = off, or 15..240, default 30; needs realtime on). The tool sends the settings revision it read; a 409 (another save landed first) comes back as { conflict: true, current } with nothing overwritten. A 422 names the invalid field.", {
|
|
113
|
+
realtime: z.boolean().optional().describe("Team Chat realtime delivery on/off"),
|
|
114
|
+
realtimePresence: z.boolean().optional().describe("Online/away presence over the realtime pipe (needs realtime)"),
|
|
115
|
+
quietHours: z.object({
|
|
116
|
+
start: z.string().regex(HHMM).describe("HH:MM in the account timezone"),
|
|
117
|
+
end: z.string().regex(HHMM).describe("HH:MM in the account timezone (may be earlier than start)"),
|
|
118
|
+
}).strict().nullable().optional().describe("Quiet-hours window, or null for none"),
|
|
119
|
+
digestMinutes: z.number().int().refine((n) => n === 0 || (n >= 15 && n <= 240), { message: "digestMinutes is 0 (off) or 15..240" }).optional().describe("Missed-message digest window in minutes: 0 = off, 15..240"),
|
|
120
|
+
}, async (params) => {
|
|
121
|
+
const teamChat = {};
|
|
122
|
+
for (const k of ["realtime", "realtimePresence", "quietHours", "digestMinutes"])
|
|
123
|
+
if (params[k] !== undefined)
|
|
124
|
+
teamChat[k] = params[k];
|
|
125
|
+
if (Object.keys(teamChat).length === 0)
|
|
126
|
+
throw new Error("nothing to change: pass at least one of realtime, realtimePresence, quietHours, digestMinutes");
|
|
127
|
+
return patchGeneralSettings(client, { teamChat });
|
|
128
|
+
}));
|
|
55
129
|
}
|
|
@@ -24,19 +24,33 @@ export function registerWorkflowSandboxTools(server, client) {
|
|
|
24
24
|
});
|
|
25
25
|
return formatResult(result);
|
|
26
26
|
});
|
|
27
|
-
server.tool("conduyt_workflow_simulate", "Dry-run a saved automation against a real or stub contact. Returns a step-by-step trace of what WOULD happen — no messages sent, no tags applied, no side effects. Checks compliance (DNC, opt-outs), provider readiness (SMS/email configured), and condition evaluation. Use this to verify a workflow does what you expect before publishing.", {
|
|
27
|
+
server.tool("conduyt_workflow_simulate", "Dry-run a saved automation against a real or stub contact. Returns a step-by-step trace of what WOULD happen — no messages sent, no tags applied, no side effects. Checks compliance (DNC, opt-outs), provider readiness (SMS/email configured), and condition evaluation. Use this to verify a workflow does what you expect before publishing. A deal occurrence needs dealId and a company occurrence needs companyId (see those fields). executionId (optional, admin or owner only) replays a stored run of this automation and supplies ONE fact, the company carried by that run's payload; the run must belong to this account and this automation with the same trigger event, contact and deal as the preview, and without executionId on a non-company occurrence the preview is a synthetic sample whose company rules evaluate as unknown/absent, which the response states.", {
|
|
28
28
|
automationId: z.string().describe("Automation UUID to simulate"),
|
|
29
29
|
contactId: z.string().optional().describe("Contact UUID to simulate against. If omitted, uses a stub contact with empty fields."),
|
|
30
|
-
|
|
30
|
+
dealId: z.string().optional().describe("The event's deal for a DEAL occurrence (deal.created / stage_changed / won / lost / ...): REQUIRED, 400 without it (the contact's newest deal is never a substitute); must be a deal you can see (deals.view and pipeline visibility), 404 otherwise; refused (400) when the selected occurrence is not a deal event (UUID)"),
|
|
31
|
+
companyId: z.string().optional().describe("The event's company for a COMPANY occurrence (company.created / updated / stage_changed / deleted / tagged / tag_removed): REQUIRED, 400 without it; needs companies.view, 404 when not found in this account; executionId does not replace it. On any other occurrence it is accepted only when it equals the company carried by the replayed executionId payload, else refused (400). With executionId on a company occurrence it must equal the replayed payload's company (400 otherwise). Company producers carry no contact, so contact filters on such an occurrence are judged as production judges them, without one (UUID)"),
|
|
32
|
+
executionId: z.string().optional().describe("Optional id of a stored run of THIS automation to replay (admin or owner only). The replay supplies ONE fact: the company carried by that run's stored payload, read the way the live runner reads it; nothing else about the subject comes from the run. The run must belong to this account and this automation, carry the same trigger event as the occurrence being previewed, and the same contact and deal as the preview; otherwise 404 (not found, or a deal run hidden from API keys without the deals scope) or 400 (other event or other subject). Runs that carry a deal are hidden from API keys without the deals scope. On a COMPANY occurrence companyId stays required and executionId does not replace it (it must equal the replayed payload's company). Omitted, on a non-company occurrence: the preview is a synthetic sample, company rules evaluate as unknown/absent, and the response says so (UUID)"),
|
|
33
|
+
}, async ({ automationId, contactId, dealId, companyId, executionId }) => {
|
|
31
34
|
if (!UUID_RE.test(automationId)) {
|
|
32
35
|
return formatResult({ error: "automationId must be a valid UUID" });
|
|
33
36
|
}
|
|
34
37
|
if (contactId && !UUID_RE.test(contactId)) {
|
|
35
38
|
return formatResult({ error: "contactId must be a valid UUID" });
|
|
36
39
|
}
|
|
40
|
+
for (const [name, value] of [["dealId", dealId], ["companyId", companyId], ["executionId", executionId]]) {
|
|
41
|
+
if (value !== undefined && !UUID_RE.test(value)) {
|
|
42
|
+
return formatResult({ error: `${name} must be a valid UUID` });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
37
45
|
const body = {};
|
|
38
46
|
if (contactId)
|
|
39
47
|
body.contactId = contactId;
|
|
48
|
+
if (dealId !== undefined)
|
|
49
|
+
body.dealId = dealId;
|
|
50
|
+
if (companyId !== undefined)
|
|
51
|
+
body.companyId = companyId;
|
|
52
|
+
if (executionId !== undefined)
|
|
53
|
+
body.executionId = executionId;
|
|
40
54
|
const result = await client.post(`/api/v1/automations/${automationId}/dry-run`, body);
|
|
41
55
|
return formatResult(result);
|
|
42
56
|
});
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-10-
|
|
3
|
-
"toolCount":
|
|
4
|
-
"totalEndpoints":
|
|
2
|
+
"generatedAt": "2026-10-20T15:03:32.151Z",
|
|
3
|
+
"toolCount": 359,
|
|
4
|
+
"totalEndpoints": 1102,
|
|
5
5
|
"counts": {
|
|
6
|
-
"covered":
|
|
7
|
-
"excluded":
|
|
8
|
-
"planned":
|
|
6
|
+
"covered": 317,
|
|
7
|
+
"excluded": 328,
|
|
8
|
+
"planned": 457,
|
|
9
9
|
"uncategorized": 0
|
|
10
10
|
},
|
|
11
11
|
"matrix": {
|
|
@@ -1310,6 +1310,18 @@
|
|
|
1310
1310
|
"status": "planned",
|
|
1311
1311
|
"reason": "team chat read/post for agents"
|
|
1312
1312
|
},
|
|
1313
|
+
"GET /api/v1/chat/channels/:id/notifications": {
|
|
1314
|
+
"status": "covered",
|
|
1315
|
+
"tools": [
|
|
1316
|
+
"conduyt_get_chat_notifications"
|
|
1317
|
+
]
|
|
1318
|
+
},
|
|
1319
|
+
"PATCH /api/v1/chat/channels/:id/notifications": {
|
|
1320
|
+
"status": "covered",
|
|
1321
|
+
"tools": [
|
|
1322
|
+
"conduyt_set_chat_notifications"
|
|
1323
|
+
]
|
|
1324
|
+
},
|
|
1313
1325
|
"GET /api/v1/chat/channels/:id/pins": {
|
|
1314
1326
|
"status": "planned",
|
|
1315
1327
|
"reason": "team chat read/post for agents"
|
|
@@ -1657,10 +1669,18 @@
|
|
|
1657
1669
|
"status": "planned",
|
|
1658
1670
|
"reason": "conversation thread surfaces"
|
|
1659
1671
|
},
|
|
1672
|
+
"GET /api/v1/cron/analytics-account-sync": {
|
|
1673
|
+
"status": "excluded",
|
|
1674
|
+
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
1675
|
+
},
|
|
1660
1676
|
"GET /api/v1/cron/apply-contact-stamps": {
|
|
1661
1677
|
"status": "excluded",
|
|
1662
1678
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
1663
1679
|
},
|
|
1680
|
+
"GET /api/v1/cron/apply-product-signals": {
|
|
1681
|
+
"status": "excluded",
|
|
1682
|
+
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
1683
|
+
},
|
|
1664
1684
|
"GET /api/v1/cron/booking-reminders": {
|
|
1665
1685
|
"status": "excluded",
|
|
1666
1686
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
@@ -1753,6 +1773,10 @@
|
|
|
1753
1773
|
"status": "excluded",
|
|
1754
1774
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
1755
1775
|
},
|
|
1776
|
+
"GET /api/v1/cron/process-ad-conversions": {
|
|
1777
|
+
"status": "excluded",
|
|
1778
|
+
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
1779
|
+
},
|
|
1756
1780
|
"GET /api/v1/cron/process-auto-dispositions": {
|
|
1757
1781
|
"status": "excluded",
|
|
1758
1782
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
@@ -2073,6 +2097,10 @@
|
|
|
2073
2097
|
"status": "excluded",
|
|
2074
2098
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
2075
2099
|
},
|
|
2100
|
+
"GET /api/v1/cron/team-chat-digest": {
|
|
2101
|
+
"status": "excluded",
|
|
2102
|
+
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
2103
|
+
},
|
|
2076
2104
|
"GET /api/v1/cron/trial-lifecycle-emails": {
|
|
2077
2105
|
"status": "excluded",
|
|
2078
2106
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
@@ -3157,6 +3185,10 @@
|
|
|
3157
3185
|
"conduyt_mailchimp_retry_unsubscribes"
|
|
3158
3186
|
]
|
|
3159
3187
|
},
|
|
3188
|
+
"POST /api/v1/integrations/posthog/intent": {
|
|
3189
|
+
"status": "planned",
|
|
3190
|
+
"reason": "integration management"
|
|
3191
|
+
},
|
|
3160
3192
|
"GET /api/v1/integrations/slack/channels": {
|
|
3161
3193
|
"status": "planned",
|
|
3162
3194
|
"reason": "integration management"
|
|
@@ -3331,6 +3363,10 @@
|
|
|
3331
3363
|
"conduyt_check_message"
|
|
3332
3364
|
]
|
|
3333
3365
|
},
|
|
3366
|
+
"POST /api/v1/messages/check-links": {
|
|
3367
|
+
"status": "planned",
|
|
3368
|
+
"reason": "message surfaces beyond send tools"
|
|
3369
|
+
},
|
|
3334
3370
|
"GET /api/v1/messages/sms/:id": {
|
|
3335
3371
|
"status": "planned",
|
|
3336
3372
|
"reason": "message surfaces beyond send tools"
|
|
@@ -3712,6 +3748,12 @@
|
|
|
3712
3748
|
"status": "planned",
|
|
3713
3749
|
"reason": "quick note surfaces"
|
|
3714
3750
|
},
|
|
3751
|
+
"GET /api/v1/realtime/token": {
|
|
3752
|
+
"status": "covered",
|
|
3753
|
+
"tools": [
|
|
3754
|
+
"conduyt_get_chat_realtime_token"
|
|
3755
|
+
]
|
|
3756
|
+
},
|
|
3715
3757
|
"POST /api/v1/remote-assist/end": {
|
|
3716
3758
|
"status": "excluded",
|
|
3717
3759
|
"reason": "realtime browser RTC surface"
|