conduyt-mcp 4.94.0 → 4.96.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/board-settings.js +5 -4
- package/dist/tools/bulk.js +2 -2
- package/dist/tools/chat.js +79 -5
- package/dist/tools/deals.js +2 -2
- package/dist/tools/pipelines.d.ts +1 -1
- package/dist/tools/pipelines.js +4 -4
- package/package.json +1 -1
- package/scripts/coverage-matrix.json +40 -6
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ MCP server for [Conduyt CRM](https://conduyt.app) — expose your CRM as AI-acce
|
|
|
33
33
|
| `conduyt_pipeline_health` | Pipeline health for one pipeline or all visible active pipelines (`GET /deals/inspection`): the six checks over open deals by money at risk; `check` adds that check's deals, paged by `cursor` (or `offset`). Cursor 400s carry `health_cursor_invalid` / `_context` / `_blank` / `_needs_check`, `health_filter_conflict` |
|
|
34
34
|
| `conduyt_pipeline_health_fix_deal` | Pipeline health quick fix for a deal row (`PATCH /deals/{id}` with `checkOrigin`): `stale` = `stageId` (+ `expectedStageId`), `unassigned` = `assignedTo`, `missing_close_date` = `expectedCloseDate`, `missing_contact` = `contactId`; `expectedUpdatedAt` required. Refusals verbatim, never retried: 409 `deal_changed` / `deal_check_cleared` (with the deal's current health row), uncoded 409s, 400 `health_fix_mismatch` |
|
|
35
35
|
| `conduyt_pipeline_health_fix_next_action` | Pipeline health "Set next action" (`POST /tasks` with `checkOrigin` `high_value_idle` / `needs_action`, `dealId`, `expectedDealUpdatedAt`, optional `idempotencyKey` sent as the `Idempotency-Key` header). At most one non-done quick-fix task per deal; refusals verbatim, never retried: 409 `deal_changed` / `deal_has_next_action` / `deal_check_cleared`, 400 `health_fix_mismatch`, idempotency 409/503 |
|
|
36
|
-
| `conduyt_get_stage_rules` / `conduyt_set_stage_rules` | Read / set one stage's rule (`requiredFields` incl. `custom:<fieldKey>`, `requiredDocuments` 0-20, lost-reason label and options, `lostReasonAllowOther`); other stages untouched; revision-aware, retried once on a 409 |
|
|
36
|
+
| `conduyt_get_stage_rules` / `conduyt_set_stage_rules` | Read / set one stage's rule (`requiredFields` incl. `custom:<fieldKey>`, `requiredDocuments` 0-20, lost-reason label and options, `lostReasonAllowOther`, `entryByAutomationOnly`: only an automation can move or create a deal into the stage, every other entry is refused 422 `stage_automation_only`; `conduyt_list_pipelines` shows it on each stage); other stages untouched; revision-aware, retried once on a 409 |
|
|
37
37
|
| `conduyt_get_deal_card_fields` / `conduyt_set_deal_card_fields` | Read / replace the kanban card fields (1-8, built-ins or `custom:<fieldKey>`) |
|
|
38
38
|
| `conduyt_get_settings_revision` | The account `settingsRevision` (plus stored stage rules and rot days): pass it as `expectedSettingsRevision` to `conduyt_archive_pipeline` / `conduyt_delete_stage` so a stale one is refused 409 `stale_settings_revision` |
|
|
39
39
|
| `conduyt_get_pipeline_rot_days` / `conduyt_set_pipeline_rot_days` | Read / set the stale-in-stage threshold per pipeline (`pipelineRotDays`, 1-90 days); only the pipelines sent change; revision-aware, retried once on a 409 |
|
|
@@ -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
|
|
@@ -14,7 +14,7 @@ export const MAX_DEAL_CARD_FIELDS = 8;
|
|
|
14
14
|
export const MAX_STAGE_REQUIRED_FIELDS = 25;
|
|
15
15
|
export const MAX_STAGE_REQUIRED_DOCUMENTS = 20;
|
|
16
16
|
const CUSTOM_FIELD_RE = /^custom:[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
17
|
-
const RULE_KEYS = ["requiredFields", "requiredDocuments", "lostReasonLabel", "lostReasonOptions", "lostReasonAllowOther"];
|
|
17
|
+
const RULE_KEYS = ["requiredFields", "requiredDocuments", "lostReasonLabel", "lostReasonOptions", "lostReasonAllowOther", "entryByAutomationOnly"];
|
|
18
18
|
export const MIN_PIPELINE_ROT_DAYS = 1;
|
|
19
19
|
export const MAX_PIPELINE_ROT_DAYS = 90;
|
|
20
20
|
const requiredField = z.union([
|
|
@@ -27,7 +27,7 @@ const cardField = z.union([
|
|
|
27
27
|
]);
|
|
28
28
|
const isObj = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
29
29
|
export function registerBoardSettingsTools(server, client) {
|
|
30
|
-
strictInput(server.tool("conduyt_get_stage_rules", "Read the per-stage rules (settings.pipelineStageRules) as { pipelineId: { stageId: { requiredFields, requiredDocuments, lostReasonLabel, lostReasonOptions, lostReasonAllowOther } } }, optionally for one pipeline or one stage. A deal may enter a stage only when its requiredFields are filled (built-ins value, expectedCloseDate, contactId, assignedTo, lostReason, or custom:<fieldKey> for a deal custom field) and it has at least requiredDocuments documents attached. Set one stage with conduyt_set_stage_rules. Pipelines your key cannot see are left out.", {
|
|
30
|
+
strictInput(server.tool("conduyt_get_stage_rules", "Read the per-stage rules (settings.pipelineStageRules) as { pipelineId: { stageId: { requiredFields, requiredDocuments, lostReasonLabel, lostReasonOptions, lostReasonAllowOther, entryByAutomationOnly } } }, optionally for one pipeline or one stage. A deal may enter a stage only when its requiredFields are filled (built-ins value, expectedCloseDate, contactId, assignedTo, lostReason, or custom:<fieldKey> for a deal custom field) and it has at least requiredDocuments documents attached. entryByAutomationOnly: true (absent = off) means only an automation run can move or create a deal INTO the stage; every other entry, admins and API keys included, is refused 422 stage_automation_only (moving out stays free). Set one stage with conduyt_set_stage_rules. Pipelines your key cannot see are left out.", {
|
|
31
31
|
pipelineId: z.string().uuid().optional().describe("Only this pipeline's stage rules"),
|
|
32
32
|
stageId: z.string().uuid().optional().describe("Only this stage's rule (needs pipelineId)"),
|
|
33
33
|
}, async ({ pipelineId, stageId }) => {
|
|
@@ -42,7 +42,7 @@ export function registerBoardSettingsTools(server, client) {
|
|
|
42
42
|
}
|
|
43
43
|
return formatResult({ data: { pipelineStageRules: out } });
|
|
44
44
|
}));
|
|
45
|
-
strictInput(server.tool("conduyt_set_stage_rules", "Set the rule of ONE stage (settings.pipelineStageRules[pipelineId][stageId]; admin key, settings:edit). Only the fields you pass change: they are merged into that stage's current rule, and every other stage and pipeline is left exactly as stored. requiredFields REPLACES the stage's list (max 25 entries; built-ins value, expectedCloseDate, contactId, assignedTo, lostReason, or custom:<fieldKey> for one of the account's DEAL custom fields; [] clears it); requiredDocuments is the number of documents that must be attached before a deal may enter the stage (integer 0-20); lostReasonLabel (1-60 chars) and lostReasonOptions (max 50, trimmed and de-duplicated by the API) configure the stage's lost-reason prompt; lostReasonAllowOther (boolean) lets the rep also pick \"Other\" and type the reason (stored as the reason text; only takes effect while the stage has lost-reason options). Read-merge-write with the settings revision; if another save lands first it is re-read and retried once, then the conflict is returned with the current settings. An unknown custom field key, pipeline or stage is refused by the API.", {
|
|
45
|
+
strictInput(server.tool("conduyt_set_stage_rules", "Set the rule of ONE stage (settings.pipelineStageRules[pipelineId][stageId]; admin key, settings:edit). Only the fields you pass change: they are merged into that stage's current rule, and every other stage and pipeline is left exactly as stored. requiredFields REPLACES the stage's list (max 25 entries; built-ins value, expectedCloseDate, contactId, assignedTo, lostReason, or custom:<fieldKey> for one of the account's DEAL custom fields; [] clears it); requiredDocuments is the number of documents that must be attached before a deal may enter the stage (integer 0-20); lostReasonLabel (1-60 chars) and lostReasonOptions (max 50, trimmed and de-duplicated by the API) configure the stage's lost-reason prompt; lostReasonAllowOther (boolean) lets the rep also pick \"Other\" and type the reason (stored as the reason text; only takes effect while the stage has lost-reason options); entryByAutomationOnly (boolean) is \"Entry by automation only\": true means only an automation run can move or create a deal INTO the stage, and every route entry (deal update, create, bulk edit/update, inbound deals webhook, appointment outcome, public contact route, admins and API keys included) is refused 422 stage_automation_only with the message \"Only an automation can move a deal into <stage>\" (moving out stays free; for a manual correction set it false, move the deal, set it true again; it cannot be the starting stage of automatic intake deals). Read-merge-write with the settings revision; if another save lands first it is re-read and retried once, then the conflict is returned with the current settings. An unknown custom field key, pipeline or stage is refused by the API.", {
|
|
46
46
|
pipelineId: z.string().uuid().describe("Pipeline UUID"),
|
|
47
47
|
stageId: z.string().uuid().describe("Stage UUID within that pipeline"),
|
|
48
48
|
requiredFields: z.array(requiredField).max(MAX_STAGE_REQUIRED_FIELDS).optional().describe("Fields a deal must have filled to enter the stage: value, expectedCloseDate, contactId, assignedTo, lostReason, or custom:<deal custom field key>. Replaces the list."),
|
|
@@ -50,10 +50,11 @@ export function registerBoardSettingsTools(server, client) {
|
|
|
50
50
|
lostReasonLabel: z.string().trim().min(1).max(60).optional().describe("Label of the stage's lost-reason prompt, e.g. 'Disqualification Reason'"),
|
|
51
51
|
lostReasonOptions: z.array(z.string()).max(50).optional().describe("Dropdown options of the lost-reason prompt (replaces the list)"),
|
|
52
52
|
lostReasonAllowOther: z.boolean().optional().describe("The rep may pick 'Other' and type the reason (stored as the reason text). Needs lost-reason options to take effect."),
|
|
53
|
+
entryByAutomationOnly: z.boolean().optional().describe("Entry by automation only: when true, only an automation run can move or create a deal INTO this stage; every API / UI entry (admins and API keys included) is refused 422 stage_automation_only. Moving a deal out stays free. To make a manual correction, turn it off, move the deal, turn it back on."),
|
|
53
54
|
}, async ({ pipelineId, stageId, ...given }) => {
|
|
54
55
|
const change = Object.fromEntries(Object.entries(given).filter(([, v]) => v !== undefined));
|
|
55
56
|
if (Object.keys(change).length === 0) {
|
|
56
|
-
throw new Error("Nothing to set. Pass at least one of requiredFields, requiredDocuments, lostReasonLabel, lostReasonOptions, lostReasonAllowOther.");
|
|
57
|
+
throw new Error("Nothing to set. Pass at least one of requiredFields, requiredDocuments, lostReasonLabel, lostReasonOptions, lostReasonAllowOther, entryByAutomationOnly.");
|
|
57
58
|
}
|
|
58
59
|
return patchGeneralSettingsMerged(client, (settings) => {
|
|
59
60
|
const rules = isObj(settings.pipelineStageRules) ? settings.pipelineStageRules : {};
|
package/dist/tools/bulk.js
CHANGED
|
@@ -137,14 +137,14 @@ export function registerBulkTools(server, client) {
|
|
|
137
137
|
const result = await client.post("/api/v1/bulk/contacts/dnc", params);
|
|
138
138
|
return formatResult(result);
|
|
139
139
|
});
|
|
140
|
-
server.tool("conduyt_bulk_edit_deals", "Apply the SAME field changes to many deals at once (one updates object → all ids) — e.g. reassign a set of deals to a user. For different values per deal, use conduyt_bulk_update_deals. Requires deals:edit.", {
|
|
140
|
+
server.tool("conduyt_bulk_edit_deals", "Apply the SAME field changes to many deals at once (one updates object → all ids) — e.g. reassign a set of deals to a user. For different values per deal, use conduyt_bulk_update_deals. Requires deals:edit. A stage change into an \"Entry by automation only\" stage is refused as code stage_automation_only with the server's message \"Only an automation can move a deal into <stage>\": the failed deal's own entry in the result carries the code and the message (per item, not a top-level error); other deals are handled on their own.", {
|
|
141
141
|
ids: z.array(z.string()).describe("Deal UUIDs to edit"),
|
|
142
142
|
updates: z.record(z.any()).describe("Fields to set on every listed deal, e.g. { assignedTo: '<uuid>', stageId: '<uuid>', priority: 'high' }"),
|
|
143
143
|
}, async (params) => {
|
|
144
144
|
const result = await client.post("/api/v1/bulk/deals/edit", params);
|
|
145
145
|
return formatResult(result);
|
|
146
146
|
});
|
|
147
|
-
server.tool("conduyt_bulk_update_deals", "Apply DIFFERENT field changes per deal in one call (each row carries its own id + fields). For the same change across many, use conduyt_bulk_edit_deals. Requires deals:edit.", {
|
|
147
|
+
server.tool("conduyt_bulk_update_deals", "Apply DIFFERENT field changes per deal in one call (each row carries its own id + fields). For the same change across many, use conduyt_bulk_edit_deals. Requires deals:edit. A stage change into an \"Entry by automation only\" stage is refused as code stage_automation_only with the server's message \"Only an automation can move a deal into <stage>\": the failed deal's own entry in the result carries the code and the message (per item, not a top-level error); other deals are handled on their own.", {
|
|
148
148
|
updates: z
|
|
149
149
|
.array(z.record(z.any()))
|
|
150
150
|
.describe("Array of per-deal update objects; each MUST include 'id' plus the fields to change, e.g. [{ id: '<uuid>', value: 5000 }, …]"),
|
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
|
}
|
package/dist/tools/deals.js
CHANGED
|
@@ -199,7 +199,7 @@ export function registerDealTools(server, client) {
|
|
|
199
199
|
const result = await client.get(`/api/v1/deals/${id}`);
|
|
200
200
|
return formatResult(result);
|
|
201
201
|
});
|
|
202
|
-
server.tool("conduyt_create_deal", "Create a new deal in a pipeline with full attributes. Requires a pipeline and stage ID — use conduyt_list_pipelines to discover them. Supports currency (e.g. GBP), probability, priority, expected close date, and structured custom-field values.", {
|
|
202
|
+
server.tool("conduyt_create_deal", "Create a new deal in a pipeline with full attributes. Requires a pipeline and stage ID — use conduyt_list_pipelines to discover them. Supports currency (e.g. GBP), probability, priority, expected close date, and structured custom-field values. A stage set to \"Entry by automation only\" refuses every entry here: 422 code stage_automation_only, message \"Only an automation can move a deal into <stage>\" (admins and API keys included; moving out is free).", {
|
|
203
203
|
title: z.string().describe("Deal title (e.g. 'Acme Corp — Enterprise Plan')"),
|
|
204
204
|
pipelineId: z.string().describe("Pipeline UUID"),
|
|
205
205
|
stageId: z.string().describe("Stage UUID within the pipeline"),
|
|
@@ -218,7 +218,7 @@ export function registerDealTools(server, client) {
|
|
|
218
218
|
const result = await client.post("/api/v1/deals", params);
|
|
219
219
|
return formatResult(result);
|
|
220
220
|
});
|
|
221
|
-
server.tool("conduyt_update_deal", "Update a deal — move stage, change value/currency, set probability or expected close date, reassign, mark won/lost, or write structured custom-field values. customFields are merged into existing values (omit a key to leave it unchanged).", {
|
|
221
|
+
server.tool("conduyt_update_deal", "Update a deal — move stage, change value/currency, set probability or expected close date, reassign, mark won/lost, or write structured custom-field values. customFields are merged into existing values (omit a key to leave it unchanged). A stage set to \"Entry by automation only\" refuses every entry here: 422 code stage_automation_only, message \"Only an automation can move a deal into <stage>\" (admins and API keys included; moving out is free).", {
|
|
222
222
|
id: z.string().describe("Deal UUID"),
|
|
223
223
|
title: z.string().optional().describe("Updated title"),
|
|
224
224
|
stageId: z.string().optional().describe("Move to a new stage UUID"),
|
|
@@ -3,7 +3,7 @@ import { ConduytClient } from "../client.js";
|
|
|
3
3
|
/** The 10 palette colours a pipeline can be painted (API: PIPELINE_COLOR_PALETTE; any other value is a 400). */
|
|
4
4
|
export declare const PIPELINE_COLORS: readonly ["#3845C9", "#43DFF6", "#2DD6B5", "#22C58B", "#9BD13B", "#F2B544", "#F07A2F", "#E6465A", "#FD88C0", "#956EFA"];
|
|
5
5
|
/** The machine-readable codes the archive route answers with (the API puts `code` in the error body; the client carries it in the error text). */
|
|
6
|
-
export declare const ARCHIVE_ERROR_CODES = "A refusal carries a machine-readable code in the error text: stale_settings_revision (409, settings changed since expectedSettingsRevision was read; nothing moved), target_not_open (409, the destination's first stage is won/lost), dpp_post_close (409, the DPP feed's Post-Close pipeline), archive_too_large (409, too many open deals for one call: move or close some first), target_archived (409, the destination was archived meanwhile), intake_target (409, enabled intake creates its cards in this pipeline), pipeline_archived (409), open_deals (409, open deals but no moveOpenDealsTo), default_pipeline (409), stage_requirements (409, a deal fails the destination's stage-entry rules; nothing moved), archive_raced (409, retry), pipeline_automation_only (403). Archiving a pipeline that is already archived is idempotent: { archived: true, alreadyArchived: true }. The message is the server's, never swallowed. The call can take up to 75 s (a 75 s per-call timeout is used); if it still reports a timeout the archive may have committed: re-read with conduyt_list_pipelines / conduyt_list_archived_pipelines before retrying (a retry of a committed archive answers alreadyArchived, but its open deals have already moved).";
|
|
6
|
+
export declare const ARCHIVE_ERROR_CODES = "A refusal carries a machine-readable code in the error text: stale_settings_revision (409, settings changed since expectedSettingsRevision was read; nothing moved), target_not_open (409, the destination's first stage is won/lost), dpp_post_close (409, the DPP feed's Post-Close pipeline), archive_too_large (409, too many open deals for one call: move or close some first), target_archived (409, the destination was archived meanwhile), intake_target (409, enabled intake creates its cards in this pipeline), pipeline_archived (409), open_deals (409, open deals but no moveOpenDealsTo), default_pipeline (409), stage_requirements (409, a deal fails the destination's stage-entry rules; nothing moved), archive_raced (409, retry), pipeline_automation_only (403), stage_automation_only (422, moveOpenDealsTo lands in an \"Entry by automation only\" stage; nothing moved). Archiving a pipeline that is already archived is idempotent: { archived: true, alreadyArchived: true }. The message is the server's, never swallowed. The call can take up to 75 s (a 75 s per-call timeout is used); if it still reports a timeout the archive may have committed: re-read with conduyt_list_pipelines / conduyt_list_archived_pipelines before retrying (a retry of a committed archive answers alreadyArchived, but its open deals have already moved).";
|
|
7
7
|
/** above the archive route's 60 s transaction (up to 500 deal moves) plus post-commit events; the client default is 30 s */
|
|
8
8
|
export declare const ARCHIVE_PIPELINE_TIMEOUT_MS = 75000;
|
|
9
9
|
export declare function registerPipelineTools(server: McpServer, client: ConduytClient): void;
|
package/dist/tools/pipelines.js
CHANGED
|
@@ -5,11 +5,11 @@ import { strictInput } from "./strict.js";
|
|
|
5
5
|
export const PIPELINE_COLORS = ["#3845C9", "#43DFF6", "#2DD6B5", "#22C58B", "#9BD13B", "#F2B544", "#F07A2F", "#E6465A", "#FD88C0", "#956EFA"];
|
|
6
6
|
const pipelineColor = z.enum(PIPELINE_COLORS);
|
|
7
7
|
/** The machine-readable codes the archive route answers with (the API puts `code` in the error body; the client carries it in the error text). */
|
|
8
|
-
export const ARCHIVE_ERROR_CODES = "A refusal carries a machine-readable code in the error text: stale_settings_revision (409, settings changed since expectedSettingsRevision was read; nothing moved), target_not_open (409, the destination's first stage is won/lost), dpp_post_close (409, the DPP feed's Post-Close pipeline), archive_too_large (409, too many open deals for one call: move or close some first), target_archived (409, the destination was archived meanwhile), intake_target (409, enabled intake creates its cards in this pipeline), pipeline_archived (409), open_deals (409, open deals but no moveOpenDealsTo), default_pipeline (409), stage_requirements (409, a deal fails the destination's stage-entry rules; nothing moved), archive_raced (409, retry), pipeline_automation_only (403). Archiving a pipeline that is already archived is idempotent: { archived: true, alreadyArchived: true }. The message is the server's, never swallowed. The call can take up to 75 s (a 75 s per-call timeout is used); if it still reports a timeout the archive may have committed: re-read with conduyt_list_pipelines / conduyt_list_archived_pipelines before retrying (a retry of a committed archive answers alreadyArchived, but its open deals have already moved).";
|
|
8
|
+
export const ARCHIVE_ERROR_CODES = "A refusal carries a machine-readable code in the error text: stale_settings_revision (409, settings changed since expectedSettingsRevision was read; nothing moved), target_not_open (409, the destination's first stage is won/lost), dpp_post_close (409, the DPP feed's Post-Close pipeline), archive_too_large (409, too many open deals for one call: move or close some first), target_archived (409, the destination was archived meanwhile), intake_target (409, enabled intake creates its cards in this pipeline), pipeline_archived (409), open_deals (409, open deals but no moveOpenDealsTo), default_pipeline (409), stage_requirements (409, a deal fails the destination's stage-entry rules; nothing moved), archive_raced (409, retry), pipeline_automation_only (403), stage_automation_only (422, moveOpenDealsTo lands in an \"Entry by automation only\" stage; nothing moved). Archiving a pipeline that is already archived is idempotent: { archived: true, alreadyArchived: true }. The message is the server's, never swallowed. The call can take up to 75 s (a 75 s per-call timeout is used); if it still reports a timeout the archive may have committed: re-read with conduyt_list_pipelines / conduyt_list_archived_pipelines before retrying (a retry of a committed archive answers alreadyArchived, but its open deals have already moved).";
|
|
9
9
|
/** above the archive route's 60 s transaction (up to 500 deal moves) plus post-commit events; the client default is 30 s */
|
|
10
10
|
export const ARCHIVE_PIPELINE_TIMEOUT_MS = 75_000;
|
|
11
11
|
export function registerPipelineTools(server, client) {
|
|
12
|
-
server.tool("conduyt_list_pipelines", "List all active sales pipelines with their stages (soft-deleted stages are never included). Use this to discover pipeline and stage UUIDs before creating or moving deals. Each pipeline carries color (one of the 10 palette hex colours, or null until the settings page saves one), automationOnly and sortOrder (the board-menu order); an archived pipeline's pre-archive name is archivedFromName (see conduyt_list_archived_pipelines).", {}, async () => {
|
|
12
|
+
server.tool("conduyt_list_pipelines", "List all active sales pipelines with their stages (soft-deleted stages are never included). Use this to discover pipeline and stage UUIDs before creating or moving deals. Each stage carries entryByAutomationOnly (boolean; true = only an automation can move or create a deal INTO it, every other entry is refused 422 stage_automation_only; set it with conduyt_set_stage_rules). Each pipeline carries color (one of the 10 palette hex colours, or null until the settings page saves one), automationOnly and sortOrder (the board-menu order); an archived pipeline's pre-archive name is archivedFromName (see conduyt_list_archived_pipelines).", {}, async () => {
|
|
13
13
|
const result = await client.get("/api/v1/pipelines");
|
|
14
14
|
return formatResult(result);
|
|
15
15
|
});
|
|
@@ -107,7 +107,7 @@ export function registerPipelineTools(server, client) {
|
|
|
107
107
|
});
|
|
108
108
|
return formatResult(result);
|
|
109
109
|
});
|
|
110
|
-
server.tool("conduyt_update_stage", "Update a stage within a pipeline — rename, recolor, reorder (sortOrder), or change its won/lost/system flags. A soft-deleted stage (conduyt_delete_stage) answers 404.", {
|
|
110
|
+
server.tool("conduyt_update_stage", "Update a stage within a pipeline — rename, recolor, reorder (sortOrder), or change its won/lost/system flags. A soft-deleted stage (conduyt_delete_stage) answers 404. The \"Entry by automation only\" flag is not set here (the API reads it from the stage rules): use conduyt_set_stage_rules; conduyt_list_pipelines shows it as entryByAutomationOnly on each stage.", {
|
|
111
111
|
pipelineId: z.string().describe("Pipeline UUID"),
|
|
112
112
|
stageId: z.string().describe("Stage UUID to update"),
|
|
113
113
|
name: z.string().optional().describe("Updated stage name"),
|
|
@@ -128,7 +128,7 @@ export function registerPipelineTools(server, client) {
|
|
|
128
128
|
const result = await client.post(`/api/v1/pipelines/${pipelineId}/stages/reorder`, { order, expectedRevision });
|
|
129
129
|
return formatResult(result);
|
|
130
130
|
}));
|
|
131
|
-
strictInput(server.tool("conduyt_delete_stage", "Delete a stage from a pipeline (soft delete). Strict input: the guards are spelled moveTo and expectedSettingsRevision, any other spelling (move_to, expected_settings_revision, a typo) is InvalidParams and nothing is deleted. An EMPTY stage can always be deleted, even if deals once used it: the row is kept so deal history, activities, audit rows and reports keep its name, but the stage is hidden from every pipeline and stage list, GET / PATCH on it answer 404, there is NO restore in v1, and its name may be reused by a new stage. Its stage rule, appointment-outcome mappings and status metadata are removed with it. With moveTo (another stage of the same pipeline, same won / lost setting, not a system stage) its live deals move there and the stage is deleted in one transaction. Pass expectedSettingsRevision (the settingsRevision from conduyt_get_settings_revision, read right before) to refuse the delete when settings changed since you read them; the answer is then { id, deleted: true, settingsRevision } with the revision after the delete. A refusal is a 409 whose server message is surfaced verbatim (never swallowed, never retried): deals currently in the stage and no moveTo (\"Cannot delete stage: N deal(s) are in this stage...\"), \"Cannot delete stage: it is used by N active automation(s) (names). Change their stage or pause them first.\" (a published automation references the stage in an action, trigger, condition or wait-until), the DPP status map selects it, plus the coded refusals stale_settings_revision, pipeline_archived and intake_target (enabled intake routes deals into this stage); a moveTo that is not a valid target is a 400. Accepted window: a run parked or in flight before the delete is not tracked; at the step that targets the deleted stage it fails or is skipped with a clear reason in the execution log.", {
|
|
131
|
+
strictInput(server.tool("conduyt_delete_stage", "Delete a stage from a pipeline (soft delete). Strict input: the guards are spelled moveTo and expectedSettingsRevision, any other spelling (move_to, expected_settings_revision, a typo) is InvalidParams and nothing is deleted. An EMPTY stage can always be deleted, even if deals once used it: the row is kept so deal history, activities, audit rows and reports keep its name, but the stage is hidden from every pipeline and stage list, GET / PATCH on it answer 404, there is NO restore in v1, and its name may be reused by a new stage. Its stage rule, appointment-outcome mappings and status metadata are removed with it. With moveTo (another stage of the same pipeline, same won / lost setting, not a system stage) its live deals move there and the stage is deleted in one transaction. Pass expectedSettingsRevision (the settingsRevision from conduyt_get_settings_revision, read right before) to refuse the delete when settings changed since you read them; the answer is then { id, deleted: true, settingsRevision } with the revision after the delete. A refusal is a 409 whose server message is surfaced verbatim (never swallowed, never retried): deals currently in the stage and no moveTo (\"Cannot delete stage: N deal(s) are in this stage...\"), \"Cannot delete stage: it is used by N active automation(s) (names). Change their stage or pause them first.\" (a published automation references the stage in an action, trigger, condition or wait-until), the DPP status map selects it, plus the coded refusals stale_settings_revision, pipeline_archived and intake_target (enabled intake routes deals into this stage); a moveTo that is not a valid target is a 400, and a moveTo stage that is \"Entry by automation only\" is refused 422 stage_automation_only (code and message unchanged). Accepted window: a run parked or in flight before the delete is not tracked; at the step that targets the deleted stage it fails or is skipped with a clear reason in the execution log.", {
|
|
132
132
|
pipelineId: z.string().describe("Pipeline UUID"),
|
|
133
133
|
stageId: z.string().describe("Stage UUID to delete"),
|
|
134
134
|
moveTo: z.string().uuid().optional().describe("Another stage of the same pipeline the live deals move to (sent as move_to)"),
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-10-
|
|
3
|
-
"toolCount":
|
|
4
|
-
"totalEndpoints":
|
|
2
|
+
"generatedAt": "2026-10-21T22:15:38.973Z",
|
|
3
|
+
"toolCount": 359,
|
|
4
|
+
"totalEndpoints": 1103,
|
|
5
5
|
"counts": {
|
|
6
|
-
"covered":
|
|
7
|
-
"excluded":
|
|
8
|
-
"planned":
|
|
6
|
+
"covered": 317,
|
|
7
|
+
"excluded": 329,
|
|
8
|
+
"planned": 457,
|
|
9
9
|
"uncategorized": 0
|
|
10
10
|
},
|
|
11
11
|
"matrix": {
|
|
@@ -317,6 +317,10 @@
|
|
|
317
317
|
"status": "excluded",
|
|
318
318
|
"reason": "super-admin platform operations (Conduyt staff only)"
|
|
319
319
|
},
|
|
320
|
+
"POST /api/v1/admin/accounts/:id/checkout-sessions/close": {
|
|
321
|
+
"status": "excluded",
|
|
322
|
+
"reason": "super-admin platform operations (Conduyt staff only)"
|
|
323
|
+
},
|
|
320
324
|
"PATCH /api/v1/admin/accounts/:id/comp": {
|
|
321
325
|
"status": "excluded",
|
|
322
326
|
"reason": "super-admin platform operations (Conduyt staff only)"
|
|
@@ -1310,6 +1314,18 @@
|
|
|
1310
1314
|
"status": "planned",
|
|
1311
1315
|
"reason": "team chat read/post for agents"
|
|
1312
1316
|
},
|
|
1317
|
+
"GET /api/v1/chat/channels/:id/notifications": {
|
|
1318
|
+
"status": "covered",
|
|
1319
|
+
"tools": [
|
|
1320
|
+
"conduyt_get_chat_notifications"
|
|
1321
|
+
]
|
|
1322
|
+
},
|
|
1323
|
+
"PATCH /api/v1/chat/channels/:id/notifications": {
|
|
1324
|
+
"status": "covered",
|
|
1325
|
+
"tools": [
|
|
1326
|
+
"conduyt_set_chat_notifications"
|
|
1327
|
+
]
|
|
1328
|
+
},
|
|
1313
1329
|
"GET /api/v1/chat/channels/:id/pins": {
|
|
1314
1330
|
"status": "planned",
|
|
1315
1331
|
"reason": "team chat read/post for agents"
|
|
@@ -1761,6 +1777,10 @@
|
|
|
1761
1777
|
"status": "excluded",
|
|
1762
1778
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
1763
1779
|
},
|
|
1780
|
+
"GET /api/v1/cron/process-ad-conversions": {
|
|
1781
|
+
"status": "excluded",
|
|
1782
|
+
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
1783
|
+
},
|
|
1764
1784
|
"GET /api/v1/cron/process-auto-dispositions": {
|
|
1765
1785
|
"status": "excluded",
|
|
1766
1786
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
@@ -2081,6 +2101,10 @@
|
|
|
2081
2101
|
"status": "excluded",
|
|
2082
2102
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
2083
2103
|
},
|
|
2104
|
+
"GET /api/v1/cron/team-chat-digest": {
|
|
2105
|
+
"status": "excluded",
|
|
2106
|
+
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
2107
|
+
},
|
|
2084
2108
|
"GET /api/v1/cron/trial-lifecycle-emails": {
|
|
2085
2109
|
"status": "excluded",
|
|
2086
2110
|
"reason": "CRON_SECRET-gated infrastructure jobs; never key-callable"
|
|
@@ -3343,6 +3367,10 @@
|
|
|
3343
3367
|
"conduyt_check_message"
|
|
3344
3368
|
]
|
|
3345
3369
|
},
|
|
3370
|
+
"POST /api/v1/messages/check-links": {
|
|
3371
|
+
"status": "planned",
|
|
3372
|
+
"reason": "message surfaces beyond send tools"
|
|
3373
|
+
},
|
|
3346
3374
|
"GET /api/v1/messages/sms/:id": {
|
|
3347
3375
|
"status": "planned",
|
|
3348
3376
|
"reason": "message surfaces beyond send tools"
|
|
@@ -3724,6 +3752,12 @@
|
|
|
3724
3752
|
"status": "planned",
|
|
3725
3753
|
"reason": "quick note surfaces"
|
|
3726
3754
|
},
|
|
3755
|
+
"GET /api/v1/realtime/token": {
|
|
3756
|
+
"status": "covered",
|
|
3757
|
+
"tools": [
|
|
3758
|
+
"conduyt_get_chat_realtime_token"
|
|
3759
|
+
]
|
|
3760
|
+
},
|
|
3727
3761
|
"POST /api/v1/remote-assist/end": {
|
|
3728
3762
|
"status": "excluded",
|
|
3729
3763
|
"reason": "realtime browser RTC surface"
|