conduyt-mcp 4.21.0 → 4.23.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/dist/client.d.ts +3 -0
- package/dist/client.js +26 -0
- package/dist/tools/automations.js +28 -5
- package/dist/tools/call-flows.js +62 -2
- package/dist/tools/contacts.js +2 -0
- package/dist/tools/custom-fields.js +33 -1
- package/dist/tools/lifecycle.js +22 -9
- package/dist/tools/workflow-sandbox.js +3 -1
- package/package.json +1 -1
- package/scripts/coverage-manifest.json +10 -0
- package/scripts/coverage-matrix.json +113 -8
package/dist/client.d.ts
CHANGED
|
@@ -49,6 +49,9 @@ export declare class ConduytClient {
|
|
|
49
49
|
request(method: string, path: string, body?: unknown, opts?: RequestOptions): Promise<unknown>;
|
|
50
50
|
requestText(method: string, path: string, body?: unknown, opts?: RequestOptions): Promise<string>;
|
|
51
51
|
get(path: string, opts?: RequestOptions): Promise<unknown>;
|
|
52
|
+
/** Multipart upload (call-flow greeting audio): same auth, timeouts and
|
|
53
|
+
* response bounds as JSON calls; the body is a FormData the caller built. */
|
|
54
|
+
postMultipart(path: string, form: FormData, opts?: RequestOptions): Promise<unknown>;
|
|
52
55
|
post(path: string, body: unknown, opts?: RequestOptions): Promise<unknown>;
|
|
53
56
|
postText(path: string, body: unknown, opts?: RequestOptions): Promise<string>;
|
|
54
57
|
patch(path: string, body: unknown, opts?: RequestOptions): Promise<unknown>;
|
package/dist/client.js
CHANGED
|
@@ -235,6 +235,32 @@ export class ConduytClient {
|
|
|
235
235
|
get(path, opts) {
|
|
236
236
|
return this.request("GET", path, undefined, opts);
|
|
237
237
|
}
|
|
238
|
+
/** Multipart upload (call-flow greeting audio): same auth, timeouts and
|
|
239
|
+
* response bounds as JSON calls; the body is a FormData the caller built. */
|
|
240
|
+
async postMultipart(path, form, opts = {}) {
|
|
241
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
242
|
+
const url = `${this.baseUrl}${path}`;
|
|
243
|
+
let res;
|
|
244
|
+
try {
|
|
245
|
+
res = await fetch(url, { method: "POST", headers: { Authorization: `Bearer ${this.apiKey}` }, body: form, signal: AbortSignal.timeout(timeoutMs) });
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
throw new ConduytApiError({ message: `Conduyt API network error for POST ${path}: ${err?.message ?? String(err)}`, code: err?.name === "TimeoutError" ? "timeout" : "network", retryable: true, cause: err });
|
|
249
|
+
}
|
|
250
|
+
const text = await res.text();
|
|
251
|
+
let json = null;
|
|
252
|
+
try {
|
|
253
|
+
json = text ? JSON.parse(text) : null;
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
json = { raw: text };
|
|
257
|
+
}
|
|
258
|
+
if (!res.ok) {
|
|
259
|
+
const msg = json?.error ?? text.slice(0, 300);
|
|
260
|
+
throw new ConduytApiError({ message: `Conduyt API ${res.status}: ${msg}`, status: res.status, code: "http", retryable: res.status >= 500 });
|
|
261
|
+
}
|
|
262
|
+
return json;
|
|
263
|
+
}
|
|
238
264
|
post(path, body, opts) {
|
|
239
265
|
return this.request("POST", path, body, opts);
|
|
240
266
|
}
|
|
@@ -30,7 +30,10 @@ export function registerAutomationTools(server, client) {
|
|
|
30
30
|
graphVersion: z.number().optional().describe("Graph version — 1 (legacy array) or 2 (node graph). Default 1"),
|
|
31
31
|
schedule: z.string().optional().describe("5-field cron expression (required if triggerEvent='scheduled')"),
|
|
32
32
|
scheduleTimezone: z.string().optional().describe("IANA timezone for scheduled triggers (e.g. 'America/Los_Angeles')"),
|
|
33
|
-
triggerConditions: z
|
|
33
|
+
triggerConditions: z
|
|
34
|
+
.any()
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("Conditions that must match for the trigger to fire. Scalar filters like tagId/pipelineId/bookingPageIds, plus per-event config: for 'contact.untouched' pass { untouchedDays: N } (integer 1-365) to fire once per quiet episode when a contact has had no outreach for N+ days — without it the trigger is the minutes-scale speed-to-lead alert. Unknown keys on native events are rejected at create/publish (422)."),
|
|
34
37
|
}, async (params) => {
|
|
35
38
|
const result = await client.post("/api/v1/automations", params);
|
|
36
39
|
return formatResult(result);
|
|
@@ -40,6 +43,10 @@ export function registerAutomationTools(server, client) {
|
|
|
40
43
|
name: z.string().optional().describe("Updated name"),
|
|
41
44
|
description: z.string().optional().describe("Updated description"),
|
|
42
45
|
triggerEvent: z.string().optional().describe("Updated trigger event"),
|
|
46
|
+
triggerConditions: z
|
|
47
|
+
.any()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Updated trigger conditions (replaces the object). Same keys as create — e.g. { untouchedDays: 7 } on 'contact.untouched'. (#16 tri-surface parity: the UI and API could set this; MCP could not.)"),
|
|
43
50
|
isActive: z.boolean().optional().describe("Enable or disable the automation"),
|
|
44
51
|
actions: z.any().optional().describe("Updated actions graph"),
|
|
45
52
|
schedule: z.string().optional().describe("Updated cron schedule"),
|
|
@@ -50,13 +57,29 @@ export function registerAutomationTools(server, client) {
|
|
|
50
57
|
.optional()
|
|
51
58
|
.describe("What a NEW trigger does when the contact is already mid-sequence: 'keep' (default — skip, existing run keeps its schedule) or 'restart' (cancel the parked run and re-enroll from step 1)"),
|
|
52
59
|
}, async ({ id, ...updates }) => {
|
|
53
|
-
|
|
60
|
+
// (Conduyt Sentry r58–r60 parity) a save that replaces the actions of a LIVE workflow must carry the
|
|
61
|
+
// configRevision it was based on — fetch it first so the server can refuse a stale overwrite (409)
|
|
62
|
+
let body = updates;
|
|
63
|
+
if (updates.actions !== undefined && body.expectedConfigRevision === undefined) {
|
|
64
|
+
const current = (await client.get(`/api/v1/automations/${id}`));
|
|
65
|
+
if (typeof current?.configRevision === "number")
|
|
66
|
+
body = { ...body, expectedConfigRevision: current.configRevision };
|
|
67
|
+
}
|
|
68
|
+
const result = await client.patch(`/api/v1/automations/${id}`, body);
|
|
54
69
|
return formatResult(result);
|
|
55
70
|
});
|
|
56
|
-
server.tool("conduyt_publish_automation", "Publish an automation — copies the draft actions to the live published version. The automation must have actions defined.", {
|
|
71
|
+
server.tool("conduyt_publish_automation", "Publish an automation — copies the draft actions to the live published version. The automation must have actions defined. The publish is bound to the draft revision you reviewed (fetched automatically unless you pass expectedConfigRevision).", {
|
|
57
72
|
id: z.string().describe("Automation UUID"),
|
|
58
|
-
|
|
59
|
-
|
|
73
|
+
expectedConfigRevision: z.number().int().nonnegative().optional().describe("The configRevision of the draft you reviewed (from conduyt_get_automation); fetched automatically when omitted"),
|
|
74
|
+
}, async ({ id, expectedConfigRevision }) => {
|
|
75
|
+
// (Conduyt Sentry r59 parity) POST /publish requires the reviewed revision — an edit that landed after the
|
|
76
|
+
// review is refused (409) instead of going live unreviewed
|
|
77
|
+
let revision = expectedConfigRevision;
|
|
78
|
+
if (revision === undefined) {
|
|
79
|
+
const current = (await client.get(`/api/v1/automations/${id}`));
|
|
80
|
+
revision = typeof current?.configRevision === "number" ? current.configRevision : undefined;
|
|
81
|
+
}
|
|
82
|
+
const result = await client.post(`/api/v1/automations/${id}/publish`, { expectedConfigRevision: revision });
|
|
60
83
|
return formatResult(result);
|
|
61
84
|
});
|
|
62
85
|
server.tool("conduyt_resolve_names", "Resolve human-readable names to UUIDs. Use this before creating automations — converts tag names, pipeline names, user names, and custom field labels to their UUIDs.", {
|
package/dist/tools/call-flows.js
CHANGED
|
@@ -32,7 +32,7 @@ export function registerCallFlowTools(server, client) {
|
|
|
32
32
|
server.tool("conduyt_create_call_flow", "Create a call flow. Starts as a DRAFT — edit the graph, validate, then publish to make it live on its number.", {
|
|
33
33
|
name: z.string().describe("Flow name"),
|
|
34
34
|
phoneNumber: z.string().optional().describe("E.164 number this flow answers (must belong to the account)"),
|
|
35
|
-
draftGraph: flowGraphSchema.optional().describe("Initial flow graph — MUST have a string entryId and a nodes array (anything else is silently replaced by a default voicemail flow server-side)"),
|
|
35
|
+
draftGraph: flowGraphSchema.optional().describe("Initial flow graph — MUST have a string entryId and a nodes array (anything else is silently replaced by a default voicemail flow server-side). Node kinds: entry, greeting, agent, group (queue: holdSeconds/pressOneVoicemail/repeatRing = queue mode), menu (options[{digit,label,next}], next = no-choice path), timing (hours[{day,start,end}], timezone, next = during hours, closedNext = outside hours), function (functionId = a registered function endpoint id, timeoutSeconds 2-8, next = success, failNext = failure; the JSON response's top-level scalars become call attributes), set_attributes (assignments[{key,value}] — value may use {{contact.firstName}} / {{call.from}} / {{attributes.x}}), branch (attribute, op equals|not_equals|contains|exists|not_exists, value, next = match, elseNext = otherwise), voicemail, forward, hangup. Spoken text in greeting/menu/voicemail/hangup may use {{contact.firstName|there}}, {{call.from}}, {{attributes.key}} (publish refuses unknown references). Every branch must reach an ending — publish refuses otherwise."),
|
|
36
36
|
}, async (params) => {
|
|
37
37
|
const result = await client.post("/api/v1/call-flows", params);
|
|
38
38
|
return formatResult(result);
|
|
@@ -41,7 +41,7 @@ export function registerCallFlowTools(server, client) {
|
|
|
41
41
|
id: z.string().describe("Call flow UUID"),
|
|
42
42
|
name: z.string().optional().describe("New name"),
|
|
43
43
|
phoneNumber: z.string().optional().describe("New E.164 number"),
|
|
44
|
-
draftGraph: flowGraphSchema.optional().describe("Replacement draft graph — MUST have a string entryId and a nodes array; REQUIRES expectedDraftRevision"),
|
|
44
|
+
draftGraph: flowGraphSchema.optional().describe("Replacement draft graph — MUST have a string entryId and a nodes array; REQUIRES expectedDraftRevision. Kinds: entry, greeting, agent, group (queue.repeatRing = queue mode), menu (options[{digit,label,next}]), timing (hours, timezone, next/closedNext), voicemail, forward, hangup; every branch must reach an ending."),
|
|
45
45
|
expectedDraftRevision: z.number().int().min(0).optional().describe("draftRevision from your last conduyt_get_call_flow read (REQUIRED when replacing draftGraph)"),
|
|
46
46
|
}, async ({ id, ...updates }) => {
|
|
47
47
|
if (updates.draftGraph !== undefined && updates.expectedDraftRevision === undefined) {
|
|
@@ -139,4 +139,64 @@ export function registerCallFlowTools(server, client) {
|
|
|
139
139
|
const result = await client.post("/api/v1/ring-groups/release-number", params);
|
|
140
140
|
return formatResult(result);
|
|
141
141
|
});
|
|
142
|
+
// ── Rollout fix 2026-08-24: numbers, Twilio wiring, greeting audio ──────
|
|
143
|
+
server.tool("conduyt_list_call_flow_numbers", "The voice numbers this workspace owns in Twilio, each with what answers it in Conduyt today (claimedBy: call flow / ring group / main line) and whether its Twilio voice webhook is already wired to Conduyt. Use it to pick a free number for a flow.", {}, async () => {
|
|
144
|
+
const result = await client.get("/api/v1/call-flows/numbers");
|
|
145
|
+
return formatResult(result);
|
|
146
|
+
});
|
|
147
|
+
server.tool("conduyt_get_call_flow_wiring", "Twilio's truth for a call flow's number: the voice webhook Twilio has right now vs the inboundUrl Conduyt needs, and whether they match (wired). A published flow that is NOT wired does not receive calls.", { id: z.string().describe("Call flow UUID") }, async ({ id }) => {
|
|
148
|
+
const result = await client.get(`/api/v1/call-flows/${id}/wiring`);
|
|
149
|
+
return formatResult(result);
|
|
150
|
+
});
|
|
151
|
+
server.tool("conduyt_wire_call_flow_number", "Point the flow's number's Twilio voice webhook at Conduyt (the same step publish performs; use it when publish reported wiring.error, or after changing the number in Twilio). Admin. Returns the resulting wiring state.", { id: z.string().describe("Call flow UUID") }, async ({ id }) => {
|
|
152
|
+
const result = await client.post(`/api/v1/call-flows/${id}/wiring`, {});
|
|
153
|
+
return formatResult(result);
|
|
154
|
+
});
|
|
155
|
+
server.tool("conduyt_release_call_flow_tombstone", "Release a number still held by a deleted call flow's tombstone: restores the number's pre-Conduyt Twilio configuration (verified) and lets the tombstone go. Admin. 404 when no tombstone holds the number; 409 when Conduyt still answers it and no prior configuration is known.", { phoneNumber: z.string().describe("E.164 number held by the tombstone") }, async ({ phoneNumber }) => {
|
|
156
|
+
const result = await client.post(`/api/v1/call-flows/tombstones/release`, { phoneNumber });
|
|
157
|
+
return formatResult(result);
|
|
158
|
+
});
|
|
159
|
+
server.tool("conduyt_upload_call_flow_audio", "Upload a greeting recording (mp3, wav or m4a, up to 10 MB) for use in a greeting node's audioUrl. Pass the file as base64. Returns { url } — put it on the greeting node via conduyt_update_call_flow.", {
|
|
160
|
+
filename: z.string().describe("File name with extension, e.g. greeting.mp3"),
|
|
161
|
+
contentBase64: z.string().describe("The file bytes, base64-encoded"),
|
|
162
|
+
mimeType: z.string().optional().describe("audio/mpeg | audio/wav | audio/mp4 (inferred from the extension when omitted)"),
|
|
163
|
+
}, async ({ filename, contentBase64, mimeType }) => {
|
|
164
|
+
const ext = filename.toLowerCase().split(".").pop() ?? "";
|
|
165
|
+
const type = mimeType ?? { mp3: "audio/mpeg", wav: "audio/wav", m4a: "audio/mp4" }[ext] ?? "application/octet-stream";
|
|
166
|
+
let bytes;
|
|
167
|
+
try {
|
|
168
|
+
bytes = Buffer.from(contentBase64, "base64");
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return formatToolError("contentBase64 is not valid base64");
|
|
172
|
+
}
|
|
173
|
+
if (bytes.byteLength === 0)
|
|
174
|
+
return formatToolError("The file is empty");
|
|
175
|
+
if (bytes.byteLength > 10 * 1024 * 1024)
|
|
176
|
+
return formatToolError("The recording exceeds 10 MB");
|
|
177
|
+
const form = new FormData();
|
|
178
|
+
form.append("file", new Blob([Uint8Array.from(bytes)], { type }), filename);
|
|
179
|
+
try {
|
|
180
|
+
const result = await client.postMultipart("/api/v1/call-flows/audio", form, { timeoutMs: 60_000 });
|
|
181
|
+
return formatResult(result);
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
return formatToolError(err.message);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
// ── #32 Call Flows function endpoints (Amazon Connect "Invoke Lambda" parity) ──
|
|
188
|
+
server.tool("conduyt_list_call_flow_functions", "List the account's call-flow function endpoints — the HTTPS endpoints a flow's \"Call a function\" step may invoke mid-call (settings scope).", {}, async () => formatResult(await client.get("/api/v1/call-flows/functions")));
|
|
189
|
+
server.tool("conduyt_create_call_flow_function", "Register a call-flow function endpoint (admin). Conduyt POSTs a signed JSON snapshot of the call (X-Conduyt-Signature = sha256 HMAC of `${timestamp}.${body}` with the secret, X-Conduyt-Timestamp, Idempotency-Key callSid:version:nodeId); the endpoint replies 200 with a JSON object whose top-level strings/numbers/booleans become call attributes. https only, public hosts only, no redirects, 2-8 s timeout, 64 KB response cap. The secret is write-only.", {
|
|
190
|
+
name: z.string().min(1).max(60).describe("Display name (unique per account)"),
|
|
191
|
+
url: z.string().url().describe("HTTPS endpoint URL (ports 443/8443 only, public host)"),
|
|
192
|
+
secret: z.string().min(16).max(200).describe("HMAC signing secret the endpoint will verify with"),
|
|
193
|
+
}, async (params) => formatResult(await client.post("/api/v1/call-flows/functions", params)));
|
|
194
|
+
server.tool("conduyt_update_call_flow_function", "Rename a call-flow function endpoint, change its URL, or rotate its secret (admin).", {
|
|
195
|
+
id: z.string().describe("Function UUID"),
|
|
196
|
+
name: z.string().min(1).max(60).optional(),
|
|
197
|
+
url: z.string().url().optional(),
|
|
198
|
+
secret: z.string().min(16).max(200).optional().describe("New signing secret (rotation)"),
|
|
199
|
+
}, async ({ id, ...body }) => formatResult(await client.patch(`/api/v1/call-flows/functions/${id}`, body)));
|
|
200
|
+
server.tool("conduyt_delete_call_flow_function", "Remove a call-flow function endpoint (admin). Refused while a PUBLISHED flow still calls it — edit and republish that flow first.", { id: z.string().describe("Function UUID") }, async ({ id }) => formatResult(await client.del(`/api/v1/call-flows/functions/${id}`)));
|
|
201
|
+
server.tool("conduyt_test_call_flow_function", "Send a real signed test call (callSid TEST, a sample caller) to a function endpoint and report the status, latency and the attributes it would save.", { id: z.string().describe("Function UUID") }, async ({ id }) => formatResult(await client.post(`/api/v1/call-flows/functions/${id}/test`, {})));
|
|
142
202
|
}
|
package/dist/tools/contacts.js
CHANGED
|
@@ -51,6 +51,7 @@ export function registerContactTools(server, client) {
|
|
|
51
51
|
firstName: z.string().optional().describe("First name"),
|
|
52
52
|
lastName: z.string().optional().describe("Last name"),
|
|
53
53
|
phone: z.string().optional().describe("Phone number"),
|
|
54
|
+
timezone: z.string().optional().describe("IANA timezone of the lead (e.g. America/Chicago). Omit to let Conduyt derive it from ZIP → phone area code → state (the stored contact carries timezone + timezoneSource)."),
|
|
54
55
|
company: z.string().optional().describe("Company name"),
|
|
55
56
|
source: z.string().trim().min(1).max(100).optional().describe("Lead source (e.g. 'website', 'referral', 'import') — FIRST-WRITE-WINS acquisition truth for the lead; later inbound events do not overwrite it. 1-100 chars."),
|
|
56
57
|
masterStatus: z.string().trim().min(1).max(40).optional().describe("Lifecycle status of the LEAD (master record), distinct from any deal's status: 'open' (default), 'won', 'lost', 'abandoned', 'disqualified', or an account-defined custom status. A terminal status (won/lost/abandoned/disqualified) SILENCES all outbound automations for this contact — drips, sequences, and scheduled sends stop. Use conduyt_get_lifecycle_settings to see the account's configured statuses."),
|
|
@@ -69,6 +70,7 @@ export function registerContactTools(server, client) {
|
|
|
69
70
|
firstName: z.string().optional().describe("Updated first name"),
|
|
70
71
|
lastName: z.string().optional().describe("Updated last name"),
|
|
71
72
|
phone: z.string().optional().describe("Updated phone"),
|
|
73
|
+
timezone: z.string().nullable().optional().describe("IANA timezone override (e.g. America/Denver); pass null to clear the override and re-derive from ZIP → area code → state"),
|
|
72
74
|
company: z.string().optional().describe("Updated company name"),
|
|
73
75
|
source: z.string().trim().min(1).max(100).nullable().optional().describe("Correct the lead's first-touch source (normally first-write-wins and set at creation — update only to FIX bad attribution). Pass null to clear."),
|
|
74
76
|
masterStatus: z.string().trim().min(1).max(40).optional().describe("Lifecycle status of the LEAD (master record), distinct from any deal's status: 'open' (default), 'won', 'lost', 'abandoned', 'disqualified', or an account-defined custom status. A terminal status (won/lost/abandoned/disqualified) SILENCES all outbound automations for this contact — drips, sequences, and scheduled sends stop. Use conduyt_get_lifecycle_settings to see the account's configured statuses."),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { formatResult } from "../client.js";
|
|
2
|
+
import { ConduytApiError, formatResult } from "../client.js";
|
|
3
3
|
export function registerCustomFieldTools(server, client) {
|
|
4
4
|
server.tool("conduyt_list_custom_fields", "List custom field definitions. Filter by entity type (contact, deal, or company) to see available fields.", {
|
|
5
5
|
entityType: z.enum(["contact", "deal", "company"]).optional().describe("Filter by entity type"),
|
|
@@ -45,4 +45,36 @@ export function registerCustomFieldTools(server, client) {
|
|
|
45
45
|
const result = await client.del(`/api/v1/custom-fields/${id}`);
|
|
46
46
|
return formatResult(result);
|
|
47
47
|
});
|
|
48
|
+
// ─── account default columns for the contacts list (admin-managed; applied to every user on their next visit) ───
|
|
49
|
+
server.tool("conduyt_get_contact_table_defaults", "Read the account-level DEFAULT COLUMNS for the Contacts list: the column ids every user gets merged into their own layout on their next visit (each version once; users can rearrange afterwards). null = no account default (users see the built-in layout). Reads /api/v1/settings (settings:view).", {}, async () => {
|
|
50
|
+
const result = (await client.get("/api/v1/settings"));
|
|
51
|
+
const raw = result?.data?.contactTableDefaults;
|
|
52
|
+
return formatResult({ data: raw && typeof raw === "object" ? raw : null, revision: raw && typeof raw === "object" && typeof raw.revision === "string" ? raw.revision : "0", columnIds: ["name", "email", "phone", "company", "source", "tags", "score", "intent", "stage", "createdAt", "lastActivity", "assignedTo", "address", "city", "state", "zip", "custom:<fieldKey>"] });
|
|
53
|
+
});
|
|
54
|
+
server.tool("conduyt_set_contact_table_defaults", "Set the account-level DEFAULT COLUMNS for the Contacts list (settings:edit). Every user's saved layout gains the columns it lacks on their next visit (appended; nothing they already show moves); at most 8 ids; custom fields as 'custom:<fieldKey>' (the key must exist). The write is revision-fenced: by default the tool reads the current row and names its `revision` (version@epoch); pass expectedRevision to fence on a row you read earlier. A concurrent change answers { conflict: true, current } — re-read and decide, nothing was overwritten.", {
|
|
55
|
+
columns: z
|
|
56
|
+
.array(z.string().min(1).max(120))
|
|
57
|
+
.max(8)
|
|
58
|
+
.describe("Column ids in display order, e.g. ['createdAt','name','email','phone','company','source','tags','stage','lastActivity']; custom fields as 'custom:<fieldKey>'"),
|
|
59
|
+
expectedRevision: z.string().min(1).max(80).optional().describe("The row `revision` this set was derived from (version@epoch, or '0' when never configured). Omitted: the tool reads the current row and uses its revision."),
|
|
60
|
+
}, async (params) => {
|
|
61
|
+
// (default columns r10, Codex r9 m1) tri-surface parity with the browser: the PATCH contract REQUIRES the server revision
|
|
62
|
+
const readRevision = async () => {
|
|
63
|
+
const result = (await client.get("/api/v1/settings"));
|
|
64
|
+
const raw = result?.data?.contactTableDefaults;
|
|
65
|
+
return raw && typeof raw === "object" && typeof raw.revision === "string" && raw.revision ? raw.revision : "0";
|
|
66
|
+
};
|
|
67
|
+
const expectedRevision = params.expectedRevision ?? (await readRevision());
|
|
68
|
+
try {
|
|
69
|
+
const result = await client.patch("/api/v1/settings", { contactTableDefaults: { columns: params.columns, expectedRevision } });
|
|
70
|
+
return formatResult(result);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
if (err instanceof ConduytApiError && err.status === 409) {
|
|
74
|
+
const current = (await client.get("/api/v1/settings"));
|
|
75
|
+
return formatResult({ conflict: true, message: err.message, expectedRevision, current: current?.data?.contactTableDefaults ?? null, hint: "the account defaults changed since that revision was read; nothing was overwritten — re-read `current`, merge your intent, and call again with its revision" });
|
|
76
|
+
}
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
48
80
|
}
|
package/dist/tools/lifecycle.js
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { formatResult } from "../client.js";
|
|
2
|
+
import { ConduytApiError, formatResult } from "../client.js";
|
|
3
|
+
/** (default columns r18, Codex r17 h1) every general-settings PATCH names the revision it loaded: read it, save, and on a 409 hand
|
|
4
|
+
* the conflict back with the current settings so the caller re-derives its change (tri-surface parity with the browser) */
|
|
5
|
+
async function patchGeneralSettings(client, body) {
|
|
6
|
+
const read = async () => {
|
|
7
|
+
const result = (await client.get("/api/v1/settings"));
|
|
8
|
+
return typeof result?.data?.settingsRevision === "string" ? result.data.settingsRevision : null;
|
|
9
|
+
};
|
|
10
|
+
const expectedSettingsRevision = await read();
|
|
11
|
+
try {
|
|
12
|
+
return formatResult(await client.patch("/api/v1/settings", { ...body, ...(expectedSettingsRevision ? { expectedSettingsRevision } : {}) }));
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
if (err instanceof ConduytApiError && err.status === 409) {
|
|
16
|
+
const current = (await client.get("/api/v1/settings"));
|
|
17
|
+
return formatResult({ conflict: true, message: err.message, expectedSettingsRevision, current: current?.data ?? null, hint: "the account settings changed since that revision was read — re-derive the change from `current` and call again" });
|
|
18
|
+
}
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
3
22
|
/**
|
|
4
23
|
* Lifecycle v2 settings (master lead status + intake deals). The master lead
|
|
5
24
|
* is the acquisition truth (first-write-wins source, one lifecycle status);
|
|
@@ -48,17 +67,11 @@ export function registerLifecycleTools(server, client) {
|
|
|
48
67
|
.enum(["always", "if_no_open", "never"])
|
|
49
68
|
.optional()
|
|
50
69
|
.describe("Policy for contacts that already exist (default 'if_no_open')"),
|
|
51
|
-
}, async (params) => {
|
|
52
|
-
const result = await client.patch("/api/v1/settings", { intakeDeals: params });
|
|
53
|
-
return formatResult(result);
|
|
54
|
-
});
|
|
70
|
+
}, async (params) => patchGeneralSettings(client, { intakeDeals: params }));
|
|
55
71
|
server.tool("conduyt_update_master_statuses", "Set the account's custom lead lifecycle statuses. The five defaults (open, won, lost, abandoned, disqualified) always exist and cannot be removed; this REPLACES the custom list on top of them (max 20 names, 1-40 chars each, letters/numbers/spaces/-/_ only). Terminal statuses stop all outbound automation for a contact. Requires settings:edit.", {
|
|
56
72
|
statuses: z
|
|
57
73
|
.array(masterStatusName)
|
|
58
74
|
.max(20)
|
|
59
75
|
.describe("Custom status names to make available alongside the defaults, e.g. ['nurturing', 'in underwriting']"),
|
|
60
|
-
}, async ({ statuses }) => {
|
|
61
|
-
const result = await client.patch("/api/v1/settings", { masterStatuses: statuses });
|
|
62
|
-
return formatResult(result);
|
|
63
|
-
});
|
|
76
|
+
}, async ({ statuses }) => patchGeneralSettings(client, { masterStatuses: statuses }));
|
|
64
77
|
}
|
|
@@ -77,7 +77,9 @@ export function registerWorkflowSandboxTools(server, client) {
|
|
|
77
77
|
message: "Workflow has warnings and allowWarnings is false. Fix warnings or set allowWarnings to true.",
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
// (Conduyt Sentry r59 parity) the publish carries the revision of the draft that was just validated
|
|
81
|
+
const current = (await client.get(`/api/v1/automations/${automationId}`));
|
|
82
|
+
const published = await client.post(`/api/v1/automations/${automationId}/publish`, { expectedConfigRevision: typeof current?.configRevision === "number" ? current.configRevision : undefined });
|
|
81
83
|
return formatResult({
|
|
82
84
|
promoted: true,
|
|
83
85
|
warnings: hasWarnings ? validation.warnings : [],
|
package/package.json
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
"planned": "callable in principle; a typed tool is wanted but not built yet"
|
|
6
6
|
},
|
|
7
7
|
"rules": [
|
|
8
|
+
{
|
|
9
|
+
"match": "GET /api/v1/call-flows/functions/:id",
|
|
10
|
+
"status": "planned",
|
|
11
|
+
"reason": "single-function read; the list tool plus update/delete/test exist, a typed get is wanted but not built (4.23.0)"
|
|
12
|
+
},
|
|
8
13
|
{
|
|
9
14
|
"match": "/api/health",
|
|
10
15
|
"status": "excluded",
|
|
@@ -509,6 +514,11 @@
|
|
|
509
514
|
"match": "POST /api/v1/call-flows/audio",
|
|
510
515
|
"status": "excluded",
|
|
511
516
|
"reason": "multipart file upload (IVR audio) - not expressible as a text MCP tool; agents reference existing audio by id from conduyt_get_call_flow"
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
"match": "GET /api/v1/ops/capacity",
|
|
520
|
+
"status": "planned",
|
|
521
|
+
"reason": "operator capacity dashboard (#51, shipped 2026-09) \u2014 read-only queue/park telemetry; a typed conduyt_capacity_snapshot tool is agent-relevant for throughput questions"
|
|
512
522
|
}
|
|
513
523
|
]
|
|
514
524
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-
|
|
3
|
-
"toolCount":
|
|
4
|
-
"totalEndpoints":
|
|
2
|
+
"generatedAt": "2026-09-04T21:17:21.600Z",
|
|
3
|
+
"toolCount": 201,
|
|
4
|
+
"totalEndpoints": 893,
|
|
5
5
|
"counts": {
|
|
6
|
-
"covered":
|
|
7
|
-
"excluded":
|
|
8
|
-
"planned":
|
|
6
|
+
"covered": 186,
|
|
7
|
+
"excluded": 263,
|
|
8
|
+
"planned": 444,
|
|
9
9
|
"uncategorized": 0
|
|
10
10
|
},
|
|
11
11
|
"matrix": {
|
|
@@ -569,6 +569,8 @@
|
|
|
569
569
|
"status": "covered",
|
|
570
570
|
"tools": [
|
|
571
571
|
"conduyt_get_automation",
|
|
572
|
+
"conduyt_update_automation",
|
|
573
|
+
"conduyt_publish_automation",
|
|
572
574
|
"conduyt_workflow_promote"
|
|
573
575
|
]
|
|
574
576
|
},
|
|
@@ -1044,16 +1046,74 @@
|
|
|
1044
1046
|
"conduyt_validate_call_flow"
|
|
1045
1047
|
]
|
|
1046
1048
|
},
|
|
1049
|
+
"GET /api/v1/call-flows/:id/wiring": {
|
|
1050
|
+
"status": "covered",
|
|
1051
|
+
"tools": [
|
|
1052
|
+
"conduyt_get_call_flow_wiring"
|
|
1053
|
+
]
|
|
1054
|
+
},
|
|
1055
|
+
"POST /api/v1/call-flows/:id/wiring": {
|
|
1056
|
+
"status": "covered",
|
|
1057
|
+
"tools": [
|
|
1058
|
+
"conduyt_wire_call_flow_number"
|
|
1059
|
+
]
|
|
1060
|
+
},
|
|
1047
1061
|
"POST /api/v1/call-flows/audio": {
|
|
1048
1062
|
"status": "excluded",
|
|
1049
1063
|
"reason": "multipart file upload (IVR audio) - not expressible as a text MCP tool; agents reference existing audio by id from conduyt_get_call_flow"
|
|
1050
1064
|
},
|
|
1065
|
+
"GET /api/v1/call-flows/functions": {
|
|
1066
|
+
"status": "covered",
|
|
1067
|
+
"tools": [
|
|
1068
|
+
"conduyt_list_call_flow_functions"
|
|
1069
|
+
]
|
|
1070
|
+
},
|
|
1071
|
+
"POST /api/v1/call-flows/functions": {
|
|
1072
|
+
"status": "covered",
|
|
1073
|
+
"tools": [
|
|
1074
|
+
"conduyt_create_call_flow_function"
|
|
1075
|
+
]
|
|
1076
|
+
},
|
|
1077
|
+
"GET /api/v1/call-flows/functions/:id": {
|
|
1078
|
+
"status": "planned",
|
|
1079
|
+
"reason": "single-function read; the list tool plus update/delete/test exist, a typed get is wanted but not built (4.23.0)"
|
|
1080
|
+
},
|
|
1081
|
+
"PATCH /api/v1/call-flows/functions/:id": {
|
|
1082
|
+
"status": "covered",
|
|
1083
|
+
"tools": [
|
|
1084
|
+
"conduyt_update_call_flow_function"
|
|
1085
|
+
]
|
|
1086
|
+
},
|
|
1087
|
+
"DELETE /api/v1/call-flows/functions/:id": {
|
|
1088
|
+
"status": "covered",
|
|
1089
|
+
"tools": [
|
|
1090
|
+
"conduyt_delete_call_flow_function"
|
|
1091
|
+
]
|
|
1092
|
+
},
|
|
1093
|
+
"POST /api/v1/call-flows/functions/:id/test": {
|
|
1094
|
+
"status": "covered",
|
|
1095
|
+
"tools": [
|
|
1096
|
+
"conduyt_test_call_flow_function"
|
|
1097
|
+
]
|
|
1098
|
+
},
|
|
1099
|
+
"GET /api/v1/call-flows/numbers": {
|
|
1100
|
+
"status": "covered",
|
|
1101
|
+
"tools": [
|
|
1102
|
+
"conduyt_list_call_flow_numbers"
|
|
1103
|
+
]
|
|
1104
|
+
},
|
|
1051
1105
|
"GET /api/v1/call-flows/roster": {
|
|
1052
1106
|
"status": "covered",
|
|
1053
1107
|
"tools": [
|
|
1054
1108
|
"conduyt_call_flow_roster"
|
|
1055
1109
|
]
|
|
1056
1110
|
},
|
|
1111
|
+
"POST /api/v1/call-flows/tombstones/release": {
|
|
1112
|
+
"status": "covered",
|
|
1113
|
+
"tools": [
|
|
1114
|
+
"conduyt_release_call_flow_tombstone"
|
|
1115
|
+
]
|
|
1116
|
+
},
|
|
1057
1117
|
"GET /api/v1/calls": {
|
|
1058
1118
|
"status": "covered",
|
|
1059
1119
|
"tools": [
|
|
@@ -1417,6 +1477,14 @@
|
|
|
1417
1477
|
"status": "excluded",
|
|
1418
1478
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
1419
1479
|
},
|
|
1480
|
+
"GET /api/v1/cron/capacity-monitor": {
|
|
1481
|
+
"status": "excluded",
|
|
1482
|
+
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
1483
|
+
},
|
|
1484
|
+
"POST /api/v1/cron/capacity-monitor": {
|
|
1485
|
+
"status": "excluded",
|
|
1486
|
+
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
1487
|
+
},
|
|
1420
1488
|
"GET /api/v1/cron/capture-rfc-ids": {
|
|
1421
1489
|
"status": "excluded",
|
|
1422
1490
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
@@ -1561,6 +1629,14 @@
|
|
|
1561
1629
|
"status": "excluded",
|
|
1562
1630
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
1563
1631
|
},
|
|
1632
|
+
"GET /api/v1/cron/process-unattended-leads": {
|
|
1633
|
+
"status": "excluded",
|
|
1634
|
+
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
1635
|
+
},
|
|
1636
|
+
"GET /api/v1/cron/process-wake-jobs": {
|
|
1637
|
+
"status": "excluded",
|
|
1638
|
+
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
1639
|
+
},
|
|
1564
1640
|
"GET /api/v1/cron/prune-screen-share-sessions": {
|
|
1565
1641
|
"status": "excluded",
|
|
1566
1642
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
@@ -1597,6 +1673,10 @@
|
|
|
1597
1673
|
"status": "excluded",
|
|
1598
1674
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
1599
1675
|
},
|
|
1676
|
+
"GET /api/v1/cron/reconcile-voice-releases": {
|
|
1677
|
+
"status": "excluded",
|
|
1678
|
+
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
1679
|
+
},
|
|
1600
1680
|
"GET /api/v1/cron/recover-booking-side-effects": {
|
|
1601
1681
|
"status": "excluded",
|
|
1602
1682
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
@@ -1799,6 +1879,10 @@
|
|
|
1799
1879
|
"status": "planned",
|
|
1800
1880
|
"reason": "deal subroutes beyond core CRUD (history, files, links)"
|
|
1801
1881
|
},
|
|
1882
|
+
"GET /api/v1/deals/:id/handoff-files/:fileId/download": {
|
|
1883
|
+
"status": "planned",
|
|
1884
|
+
"reason": "deal subroutes beyond core CRUD (history, files, links)"
|
|
1885
|
+
},
|
|
1802
1886
|
"GET /api/v1/deals/:id/history": {
|
|
1803
1887
|
"status": "planned",
|
|
1804
1888
|
"reason": "deal subroutes beyond core CRUD (history, files, links)"
|
|
@@ -2685,6 +2769,10 @@
|
|
|
2685
2769
|
"status": "planned",
|
|
2686
2770
|
"reason": "notification surfaces"
|
|
2687
2771
|
},
|
|
2772
|
+
"GET /api/v1/ops/capacity": {
|
|
2773
|
+
"status": "planned",
|
|
2774
|
+
"reason": "operator capacity dashboard (#51, shipped 2026-09) — read-only queue/park telemetry; a typed conduyt_capacity_snapshot tool is agent-relevant for throughput questions"
|
|
2775
|
+
},
|
|
2688
2776
|
"GET /api/v1/phone-numbers": {
|
|
2689
2777
|
"status": "planned",
|
|
2690
2778
|
"reason": "phone number inventory"
|
|
@@ -3200,14 +3288,15 @@
|
|
|
3200
3288
|
"GET /api/v1/settings": {
|
|
3201
3289
|
"status": "covered",
|
|
3202
3290
|
"tools": [
|
|
3291
|
+
"conduyt_get_contact_table_defaults",
|
|
3292
|
+
"conduyt_set_contact_table_defaults",
|
|
3203
3293
|
"conduyt_get_lifecycle_settings"
|
|
3204
3294
|
]
|
|
3205
3295
|
},
|
|
3206
3296
|
"PATCH /api/v1/settings": {
|
|
3207
3297
|
"status": "covered",
|
|
3208
3298
|
"tools": [
|
|
3209
|
-
"
|
|
3210
|
-
"conduyt_update_master_statuses"
|
|
3299
|
+
"conduyt_set_contact_table_defaults"
|
|
3211
3300
|
]
|
|
3212
3301
|
},
|
|
3213
3302
|
"GET /api/v1/settings/ai": {
|
|
@@ -3262,6 +3351,14 @@
|
|
|
3262
3351
|
"status": "planned",
|
|
3263
3352
|
"reason": "account settings read/update surfaces"
|
|
3264
3353
|
},
|
|
3354
|
+
"GET /api/v1/settings/operations": {
|
|
3355
|
+
"status": "planned",
|
|
3356
|
+
"reason": "account settings read/update surfaces"
|
|
3357
|
+
},
|
|
3358
|
+
"POST /api/v1/settings/operations": {
|
|
3359
|
+
"status": "planned",
|
|
3360
|
+
"reason": "account settings read/update surfaces"
|
|
3361
|
+
},
|
|
3265
3362
|
"GET /api/v1/settings/scheduled-export": {
|
|
3266
3363
|
"status": "covered",
|
|
3267
3364
|
"tools": [
|
|
@@ -3787,6 +3884,14 @@
|
|
|
3787
3884
|
"status": "excluded",
|
|
3788
3885
|
"reason": "inbound provider callback receivers (Twilio/Resend/etc.), not client-callable"
|
|
3789
3886
|
},
|
|
3887
|
+
"GET /api/v1/webhooks/voice/fallback": {
|
|
3888
|
+
"status": "excluded",
|
|
3889
|
+
"reason": "inbound provider callback receivers (Twilio/Resend/etc.), not client-callable"
|
|
3890
|
+
},
|
|
3891
|
+
"POST /api/v1/webhooks/voice/fallback": {
|
|
3892
|
+
"status": "excluded",
|
|
3893
|
+
"reason": "inbound provider callback receivers (Twilio/Resend/etc.), not client-callable"
|
|
3894
|
+
},
|
|
3790
3895
|
"POST /api/v1/webhooks/voice/flow-step": {
|
|
3791
3896
|
"status": "excluded",
|
|
3792
3897
|
"reason": "inbound provider callback receivers (Twilio/Resend/etc.), not client-callable"
|