conduyt-mcp 4.105.6 → 4.105.8
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 +4 -3
- package/dist/tool-annotations.js +2 -0
- package/dist/tools/ai-teammate-connect.d.ts +3 -1
- package/dist/tools/ai-teammate-connect.js +33 -7
- package/dist/tools/chat.js +18 -11
- package/package.json +1 -1
- package/scripts/coverage-manifest.json +1 -1
- package/scripts/coverage-matrix.json +46 -12
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
MCP server for [Conduyt CRM](https://conduyt.app) — expose your CRM as AI-accessible tools. Covers the core CRM surface (contacts, deals, pipelines, custom fields, automations, messaging, and more); use `conduyt_api_catalog` to discover the full set of 600+ REST endpoints.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 393 Tools
|
|
6
6
|
|
|
7
7
|
### Discovery
|
|
8
8
|
| Tool | Description |
|
|
@@ -270,18 +270,19 @@ The Mailchimp OAuth connect and callback are browser-only: connect Mailchimp in
|
|
|
270
270
|
|
|
271
271
|
The thread view (#684): `conduyt_list_chat_messages` takes `topLevel: true` (top-level messages with their reply chip `thread`, and the replies shown in the channel with `threadReply` `{ reason: aiFirstAnswer | alsoInChannel, root }`; never with `afterSeq`) and `conduyt_reply_chat_message` takes `alsoInChannel: true` ("Also send to #channel" on a person's reply). Both are sent only when true. They and the two thread tools work only while the workspace's switch is on (`conduyt_list_chat_channels` then carries `teamChatThreads: true`); while it is off each answers an error result with `code: team_chat_threads_off` and the sentence "The thread view is not switched on for this workspace (account setting teamChatThreads)." Against a server that predates the thread view: the two thread tools answer a 404 error result that says so, `topLevel` is reported as not applied (an error result, never a page of every reply), and an `alsoInChannel` reply is posted in the thread only, with a note saying so (never re-sent).
|
|
272
272
|
|
|
273
|
-
AI teammate turns (#802, the app listener's #655 phase 1): a turn run through Codex (`conduyt agent listen --adapter codex`) carries the thread so far and, when the channel is linked, "This channel is linked to deal <id>" (read it with `conduyt_get_chat_linked_record`), and ends with exactly ONE of `conduyt_reply_chat_message` (with `idempotencyKey` + `claimToken`), `conduyt_react_chat_message` (with `claimToken`) or `conduyt_stay_silent`; a second, different one is refused 409 `turn_finished`. The inbox itself (`/chat/agents/me/*`: the event's `context.recent`, `channel.dealId` / `contactId` and the page's `budget`) is the listener's protocol and has no tool; `GET /chat/agents`
|
|
273
|
+
AI teammate turns (#802, the app listener's #655 phase 1): a turn run through Codex (`conduyt agent listen --adapter codex`) carries the thread so far and, when the channel is linked, "This channel is linked to deal <id>" (read it with `conduyt_get_chat_linked_record`), and ends with exactly ONE of `conduyt_reply_chat_message` (with `idempotencyKey` + `claimToken`), `conduyt_react_chat_message` (with `claimToken`) or `conduyt_stay_silent`; a second, different one is refused 409 `turn_finished`. The inbox itself (`/chat/agents/me/*`: the event's `context.recent`, `channel.dealId` / `contactId` and the page's `budget`) is the listener's protocol and has no tool; (#1153) `GET /chat/agents` answers an owner's or admin's key the teammate rows of `conduyt_list_ai_teammates` (no `turnsToday` / `dailyBudget`: those are the Settings session's): the daily budget is visible in Settings > AI and in the listener's log.
|
|
274
274
|
|
|
275
275
|
### A Grok Bot as an AI teammate (#1140)
|
|
276
276
|
| Tool | Description |
|
|
277
277
|
|------|-------------|
|
|
278
|
+
| `conduyt_list_ai_teammates` | (#1153) The workspace's AI teammates (`GET /chat/agents`, an owner's or admin's key, the read tier is enough): `[{ id, handle, name, provider, state, connected, connection, createdAt }]`, nothing secret. Name a teammate to the tools below by its id or its @handle |
|
|
278
279
|
| `conduyt_connect_ai_teammate` | Step 1 (`POST /chat/agents/{id}/connect`): the message to paste into Grok Bot and the teammate's token, returned ONCE (`{ token, tokenPrefix, message, toolsUrl, agent }`); the token goes into Grok Bot's masked connector field, never into a chat. Every call revokes the earlier tokens. The calling key needs the write tier and the chat and deals scopes |
|
|
279
280
|
| `conduyt_set_ai_teammate_webhook` | Step 3 (`PUT /chat/agents/{id}/webhook`): `paste` (the block Grok Bot shows) OR `address` + `key`; checked, stored (the key encrypted), tested at once: `{ stored, test, status }`. The key is write-only: never returned, and any returned text has it replaced by `[hidden]`. A test that was not accepted is an error result with its one sentence (`key_refused`, `routine_gone`, `unreachable`, `not_accepted`, `rate_limited`; `teammate_not_live` is a 409 before any test) |
|
|
280
281
|
| `conduyt_test_ai_teammate_webhook` | The card's Test button (`POST /chat/agents/{id}/webhook/test`): the hello again to the stored routine; `{ test, status }` |
|
|
281
282
|
| `conduyt_get_ai_teammate_webhook` | The status the card polls (`GET /chat/agents/{id}/webhook`): `state` not_connected / waiting_for_hello / listening / not_listening, `problem { code, message }`, the address masked; poll it after a test until listening |
|
|
282
283
|
| `conduyt_disconnect_ai_teammate` | `DELETE /chat/agents/{id}/webhook`: tokens revoked, the routine's key cleared, pending deliveries cancelled; the teammate and its messages stay; never blocked by billing |
|
|
283
284
|
|
|
284
|
-
Owners and admins, with a person's API key; an AI teammate's own key is refused. The teammate is named by its id (a UUID)
|
|
285
|
+
Owners and admins, with a person's API key; an AI teammate's own key is refused. The teammate is named by its id (a UUID) or its @handle (`conduyt_list_ai_teammates` lists both; the server resolves the handle). Against a server without these routes (an HTML 404) each tool answers "This Conduyt server does not have the Grok Bot connection yet (it predates it); connect the teammate in Settings > AI instead."
|
|
285
286
|
|
|
286
287
|
The remaining Team Chat mutations (edit/delete, pins, claim/handoff and typing/presence writes) are not wrapped yet; sends, thread replies, reactions and direct uploads are wrapped. `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.
|
|
287
288
|
|
package/dist/tool-annotations.js
CHANGED
|
@@ -192,6 +192,8 @@ export const TOOL_ANNOTATIONS = {
|
|
|
192
192
|
conduyt_get_chat_thread: update("Get Chat Thread"),
|
|
193
193
|
// (#1140 A3) the Grok Bot teammate connect tools: connect mints a token and revokes the earlier ones on every call; set stores the
|
|
194
194
|
// routine key (replacing the last) and sends a test each call; test sends one; disconnect revokes the tokens
|
|
195
|
+
// (#1153) the teammate list: a read
|
|
196
|
+
conduyt_list_ai_teammates: ro("List AI Teammates"),
|
|
195
197
|
conduyt_connect_ai_teammate: row("Connect AI Teammate", false, true, false),
|
|
196
198
|
conduyt_set_ai_teammate_webhook: row("Set AI Teammate Webhook", false, false, false),
|
|
197
199
|
conduyt_test_ai_teammate_webhook: create("Test AI Teammate Webhook"),
|
|
@@ -14,7 +14,9 @@ export { hide } from "./routine-key-redaction.js";
|
|
|
14
14
|
*/
|
|
15
15
|
export declare const OLDER_SERVER_SENTENCE = "This Conduyt server does not have the Grok Bot connection yet (it predates it); connect the teammate in Settings > AI instead.";
|
|
16
16
|
export declare const AI_OFF_SENTENCE = "AI teammates are switched off for this workspace; an owner or admin turns them on in Settings > AI.";
|
|
17
|
-
export declare const NOT_FOUND_SENTENCE = "There is no AI teammate with that id in this workspace (it may have been revoked).";
|
|
17
|
+
export declare const NOT_FOUND_SENTENCE = "There is no AI teammate with that id or handle in this workspace (it may have been revoked); conduyt_list_ai_teammates lists them.";
|
|
18
|
+
/** (#1153) GET /chat/agents on a server from before #1153 refuses every API key with its session-only sentence */
|
|
19
|
+
export declare const OLDER_LIST_SENTENCE = "This Conduyt server does not let an API key read the AI teammate list yet (it predates it); see the teammates in Settings > AI.";
|
|
18
20
|
export declare const TEAMMATE_KEY_SENTENCE = "This is an AI teammate's own key; connect a Grok Bot with a person's API key (an owner or admin).";
|
|
19
21
|
export declare const NO_CREDENTIALS_SENTENCE = "Give the routine's address and key: paste (the block Grok Bot shows), or address and key.";
|
|
20
22
|
/** the one sentence for a refused call, and its code when the API sent one */
|
|
@@ -16,7 +16,9 @@ export { hide } from "./routine-key-redaction.js";
|
|
|
16
16
|
*/
|
|
17
17
|
export const OLDER_SERVER_SENTENCE = "This Conduyt server does not have the Grok Bot connection yet (it predates it); connect the teammate in Settings > AI instead.";
|
|
18
18
|
export const AI_OFF_SENTENCE = "AI teammates are switched off for this workspace; an owner or admin turns them on in Settings > AI.";
|
|
19
|
-
export const NOT_FOUND_SENTENCE = "There is no AI teammate with that id in this workspace (it may have been revoked).";
|
|
19
|
+
export const NOT_FOUND_SENTENCE = "There is no AI teammate with that id or handle in this workspace (it may have been revoked); conduyt_list_ai_teammates lists them.";
|
|
20
|
+
/** (#1153) GET /chat/agents on a server from before #1153 refuses every API key with its session-only sentence */
|
|
21
|
+
export const OLDER_LIST_SENTENCE = "This Conduyt server does not let an API key read the AI teammate list yet (it predates it); see the teammates in Settings > AI.";
|
|
20
22
|
export const TEAMMATE_KEY_SENTENCE = "This is an AI teammate's own key; connect a Grok Bot with a person's API key (an owner or admin).";
|
|
21
23
|
export const NO_CREDENTIALS_SENTENCE = "Give the routine's address and key: paste (the block Grok Bot shows), or address and key.";
|
|
22
24
|
function errorDetailsOf(message) {
|
|
@@ -89,10 +91,34 @@ function testOutcome(secrets) {
|
|
|
89
91
|
return connectError(problem?.message ?? "Grok Bot did not accept the test.", secrets, { status: 200, ...(problem?.code ? { code: problem.code } : {}), answer: d });
|
|
90
92
|
};
|
|
91
93
|
}
|
|
92
|
-
const
|
|
94
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
95
|
+
/** the app's handle rule (lib/agent-auth.ts normalizeAgentHandle): a letter, then 1-31 letters, digits, _ or -, case-insensitive */
|
|
96
|
+
const HANDLE_RE = /^@[a-z][a-z0-9_-]{1,31}$/i;
|
|
97
|
+
/** (#1153) the teammate: its id (a UUID) or its @handle, lower-cased; the server resolves the handle (lib/agent-webhook-routes.ts
|
|
98
|
+
* parseTeammateRef, the one place for every surface), so nothing here looks it up */
|
|
99
|
+
const TEAMMATE_ID = z.string().trim().refine((v) => UUID_RE.test(v) || HANDLE_RE.test(v), { message: "teammateId must be the teammate's id (a UUID) or its @handle" })
|
|
100
|
+
.describe("The Grok Bot teammate: its id (a UUID) or its @handle, as conduyt_list_ai_teammates lists them (the agent.id of conduyt_connect_ai_teammate's answer and the senderId of its messages are the id too)");
|
|
101
|
+
/** the path segment of a teammate: trimmed and lower-cased (handles and ids are case-insensitive), an @handle travels as %40handle */
|
|
102
|
+
const seg = (teammateId) => encodeURIComponent(teammateId.trim().toLowerCase());
|
|
93
103
|
const WHO = "Owners and admins, with a person's API key (the chat scope); an AI teammate's own key is refused (403 agent_route_refused). The teammate must have the provider Grok Bot (409 agent_not_webhook_provider otherwise). Answers 404 ai_teammates_off while AI teammates are switched off and 402 plan_required when the workspace is not entitled. Against a server that predates these routes (an HTML 404) the error says so.";
|
|
94
104
|
export function registerAiTeammateConnectTools(server, client) {
|
|
95
|
-
|
|
105
|
+
// (#1153, tri-surface parity) the workspace's AI teammates, so an agent can find the one to connect by id or @handle
|
|
106
|
+
strictInput(server.tool("conduyt_list_ai_teammates", "List the workspace's live AI teammates (GET /api/v1/chat/agents; a removed teammate is not listed), oldest first: [{ id, handle, name, provider (claude_code, codex, grok), state (listening, replying, needs_attention (#1213: listening, nothing running, and a turn in the last 24 hours failed or timed out), away), connected (a Grok Bot teammate: its connection is stored and live; any other: a listener runs now), connection (a Grok Bot teammate's not_connected, waiting_for_hello, listening or not_listening; null otherwise), knowledgeFolders (#1209: how many knowledge folders the teammate's RUNNING listeners reported, the sum over the listeners whose heartbeat beats now, 0 = none; a knowledge folder is a folder on the listener's computer the teammate reads on every turn, whoever asked, read-only, secrets-looking files excluded; it is chosen on that computer with \`conduyt agent listen\` (the start directory, --knowledge <folder> or --no-knowledge), never through Conduyt, and its path never leaves the computer), createdAt }]. Nothing secret is listed. Name a teammate to the Grok Bot tools by its id or its @handle. Owners and admins, with a person's API key (the chat scope; reading is enough); a member's key is refused (403) and an AI teammate's own key too (403 agent_route_refused). Adding a teammate stays in Settings > AI. Answers 404 ai_teammates_off while AI teammates are switched off and 402 plan_required when the workspace is not entitled; a workspace with more than 200 live teammates is refused to an API key (413 agent_list_over_ceiling; Settings > AI lists them all).", {}, async () => {
|
|
107
|
+
try {
|
|
108
|
+
const res = await client.get("/api/v1/chat/agents");
|
|
109
|
+
return connectResult(unwrap(res), []);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
if (err instanceof ConduytApiError && err.code === "http") {
|
|
113
|
+
if (err.status === 403 && /interactive browser session/i.test(err.message))
|
|
114
|
+
return connectError(OLDER_LIST_SENTENCE, [], { status: 403 });
|
|
115
|
+
const r = refusalOf(err);
|
|
116
|
+
return connectError(r.sentence, [], { status: r.status, ...(r.code ? { code: r.code } : {}) });
|
|
117
|
+
}
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
}));
|
|
121
|
+
strictInput(server.tool("conduyt_connect_ai_teammate", `Step 1 of connecting a Grok Bot as an AI teammate (POST /api/v1/chat/agents/{id}/connect). Mints the teammate's token and returns it ONCE: { token, tokenPrefix, message, toolsUrl, agent }. Give the person the message to paste into Grok Bot and the token to paste into Grok Bot's masked connector field; NEVER post the token in Team Chat or any message, and never call this again just to read it: every call revokes the teammate's earlier tokens (the card's "Get a new token"). The calling key must hold the write tier and the chat and deals scopes (403 api_key_scope_insufficient otherwise). 409 agent_listener_webhook_conflict while the teammate's listener is running; 409 teammate_not_live when its owner lost access. Step 3 is conduyt_set_ai_teammate_webhook. ${WHO}`, { teammateId: TEAMMATE_ID }, async ({ teammateId }) => attempt([], () => client.post(`/api/v1/chat/agents/${seg(teammateId)}/connect`, {}), (res) => connectResult(unwrap(res), []))));
|
|
96
122
|
strictInput(server.tool("conduyt_set_ai_teammate_webhook", `Step 3 of connecting a Grok Bot (PUT /api/v1/chat/agents/{id}/webhook): the routine's address and key, as Grok Bot shows them. Send EITHER paste (the whole block the person copied: it must hold one https address and one key) OR address and key. Conduyt checks the address (https, a public host), stores both (the key encrypted), sends the test at once (10 s) and answers { stored, test: { ok, statusCode, runUuid, problem }, status }. The key is write-only: it is never returned, and any text this tool returns has it replaced by [hidden]. A test that was not accepted is an error result { ok: false, error: <its one sentence>, status: 200, code (key_refused, routine_gone, unreachable, not_accepted, rate_limited), answer: <the whole answer> }; an accepted test leaves status Waiting for Grok Bot to say hello: poll conduyt_get_ai_teammate_webhook (every few seconds, up to about 3 minutes) until state is listening or a problem is named. Refusals: 400 with the input codes (input_conflict, address_missing, address_invalid, address_ambiguous, key_missing, key_invalid, key_ambiguous), 422 address_invalid / unreachable, 409 agent_token_missing (run conduyt_connect_ai_teammate first), agent_listener_webhook_conflict, agent_webhook_contended, teammate_not_live, 429 after 20 tests per teammate in 10 minutes, 503 while a delivery is in flight. ${WHO}`, {
|
|
97
123
|
teammateId: TEAMMATE_ID,
|
|
98
124
|
paste: z.string().min(1).max(12_000).optional().describe("The block Grok Bot shows (address and key together); not with address or key"),
|
|
@@ -105,9 +131,9 @@ export function registerAiTeammateConnectTools(server, client) {
|
|
|
105
131
|
if (paste === undefined && address === undefined)
|
|
106
132
|
return connectError(NO_CREDENTIALS_SENTENCE, secrets, { status: null, code: "address_missing" });
|
|
107
133
|
// the body as ONE literal: the contract gate reads the request fields (paste, address, key) from it
|
|
108
|
-
return attempt(secrets, () => client.put(`/api/v1/chat/agents/${teammateId}/webhook`, { ...(paste !== undefined ? { paste } : {}), ...(address !== undefined ? { address } : {}), ...(key !== undefined ? { key } : {}) }), testOutcome(secrets));
|
|
134
|
+
return attempt(secrets, () => client.put(`/api/v1/chat/agents/${seg(teammateId)}/webhook`, { ...(paste !== undefined ? { paste } : {}), ...(address !== undefined ? { address } : {}), ...(key !== undefined ? { key } : {}) }), testOutcome(secrets));
|
|
109
135
|
}));
|
|
110
|
-
strictInput(server.tool("conduyt_test_ai_teammate_webhook", `The card's Test button (POST /api/v1/chat/agents/{id}/webhook/test): sends the hello again to the stored routine (nothing is pasted); an accepted test turns a stored connection live. Answers { test, status } like conduyt_set_ai_teammate_webhook (a test that was not accepted is an error result with its sentence); then poll conduyt_get_ai_teammate_webhook for the hello. 409 agent_webhook_not_connected when nothing is stored; 429 after 20 tests per teammate in 10 minutes. ${WHO}`, { teammateId: TEAMMATE_ID }, async ({ teammateId }) => attempt([], () => client.post(`/api/v1/chat/agents/${teammateId}/webhook/test`, {}), testOutcome([]))));
|
|
111
|
-
strictInput(server.tool("conduyt_get_ai_teammate_webhook", `The Grok Bot connection's status (GET /api/v1/chat/agents/{id}/webhook), what the Settings card polls: { state (not_connected | waiting_for_hello | listening | not_listening), label, problem: { code, message } | null (the sentence of what to fix), waitingUntil, connected, address (masked), lastAttempt, tokenUsedAt, lastMessageAt }. Never carries the key. ${WHO}`, { teammateId: TEAMMATE_ID }, async ({ teammateId }) => attempt([], () => client.get(`/api/v1/chat/agents/${teammateId}/webhook`), (res) => connectResult(unwrap(res), []))));
|
|
112
|
-
strictInput(server.tool("conduyt_disconnect_ai_teammate", `Disconnect a Grok Bot teammate (DELETE /api/v1/chat/agents/{id}/webhook; never blocked by billing): its tokens are revoked, the routine's key is cleared and pending deliveries are cancelled; the teammate and its messages stay. Answers { disconnected, revokedKeys, webhook, cancelledDeliveries }. Safe to repeat. Connect it again with conduyt_connect_ai_teammate then conduyt_set_ai_teammate_webhook. ${WHO.replace(" and 402 plan_required when the workspace is not entitled", "")}`, { teammateId: TEAMMATE_ID }, async ({ teammateId }) => attempt([], () => client.del(`/api/v1/chat/agents/${teammateId}/webhook`), (res) => connectResult(unwrap(res), []))));
|
|
136
|
+
strictInput(server.tool("conduyt_test_ai_teammate_webhook", `The card's Test button (POST /api/v1/chat/agents/{id}/webhook/test): sends the hello again to the stored routine (nothing is pasted); an accepted test turns a stored connection live. Answers { test, status } like conduyt_set_ai_teammate_webhook (a test that was not accepted is an error result with its sentence); then poll conduyt_get_ai_teammate_webhook for the hello. 409 agent_webhook_not_connected when nothing is stored; 429 after 20 tests per teammate in 10 minutes. ${WHO}`, { teammateId: TEAMMATE_ID }, async ({ teammateId }) => attempt([], () => client.post(`/api/v1/chat/agents/${seg(teammateId)}/webhook/test`, {}), testOutcome([]))));
|
|
137
|
+
strictInput(server.tool("conduyt_get_ai_teammate_webhook", `The Grok Bot connection's status (GET /api/v1/chat/agents/{id}/webhook), what the Settings card polls: { state (not_connected | waiting_for_hello | listening | not_listening), label, problem: { code, message } | null (the sentence of what to fix), waitingUntil, connected, address (masked), lastAttempt, tokenUsedAt, lastMessageAt }. Never carries the key. ${WHO}`, { teammateId: TEAMMATE_ID }, async ({ teammateId }) => attempt([], () => client.get(`/api/v1/chat/agents/${seg(teammateId)}/webhook`), (res) => connectResult(unwrap(res), []))));
|
|
138
|
+
strictInput(server.tool("conduyt_disconnect_ai_teammate", `Disconnect a Grok Bot teammate (DELETE /api/v1/chat/agents/{id}/webhook; never blocked by billing): its tokens are revoked, the routine's key is cleared and pending deliveries are cancelled; the teammate and its messages stay. Answers { disconnected, revokedKeys, webhook, cancelledDeliveries }. Safe to repeat. Connect it again with conduyt_connect_ai_teammate then conduyt_set_ai_teammate_webhook. ${WHO.replace(" and 402 plan_required when the workspace is not entitled", "")}`, { teammateId: TEAMMATE_ID }, async ({ teammateId }) => attempt([], () => client.del(`/api/v1/chat/agents/${seg(teammateId)}/webhook`), (res) => connectResult(unwrap(res), []))));
|
|
113
139
|
}
|
package/dist/tools/chat.js
CHANGED
|
@@ -129,7 +129,7 @@ export function registerChatTools(server, client) {
|
|
|
129
129
|
return formatToolError("This Conduyt server did not apply topLevel: its messages carry no thread summaries (it predates the thread view). Read the channel without topLevel.");
|
|
130
130
|
return formatResult(page);
|
|
131
131
|
}));
|
|
132
|
-
strictInput(server.tool("conduyt_send_chat_message", "Send a Team Chat message as the API key's user (POST /api/v1/chat/channels/{id}/messages). The caller must be a channel member and have the communications:create permission. AI-agent keys are subject to the server's six-consecutive-agent-turn cap; a 409 agent_human_required means no message was sent (markerCreated: true on the refusal that inserted the one human-handoff line, false while the thread stays paused). A 409 agents_muted_in_channel means an admin turned \"agents respond here\" off for this channel: the post was refused, nothing was written, and it must not be retried (same shape as agent_human_required). body is plain Team Chat text (1-10,000 characters).", {
|
|
132
|
+
strictInput(server.tool("conduyt_send_chat_message", "Send a Team Chat message as the API key's user (POST /api/v1/chat/channels/{id}/messages). The caller must be a channel member and have the communications:create permission. AI-agent keys are subject to the server's six-consecutive-agent-turn cap; a 409 agent_human_required means no message was sent (markerCreated: true on the refusal that inserted the one human-handoff line, false while the thread stays paused). A 409 agents_muted_in_channel means an admin turned \"agents respond here\" off for this channel: the post was refused, nothing was written, and it must not be retried (same shape as agent_human_required). (#1213) A 409 write_outside_turn means this AI teammate is answering a turn (it holds a running claim): every post must be that turn's reply, in its thread, with conduyt_reply_chat_message and the turn's idempotencyKey + claimToken; nothing was written, do not retry. body is plain Team Chat text (1-10,000 characters).", {
|
|
133
133
|
channelId: z.string().uuid().describe("Channel UUID"),
|
|
134
134
|
body: z.string().trim().min(1).max(10_000).describe("Message text"),
|
|
135
135
|
idempotencyKey: z.string().regex(/^[A-Za-z0-9:_.-]{1,128}$/).optional().describe("Paired AI agent keys only: a key unique per agent and source turn (the listener uses reply:<source message id>). The same key returns the reply already created (200, idempotentReplay: true) instead of posting a second one; a key used in another channel is a 409 idempotency_key_reused. A person's key gets 400."),
|
|
@@ -137,14 +137,19 @@ export function registerChatTools(server, client) {
|
|
|
137
137
|
const result = await client.post(`/api/v1/chat/channels/${channelId}/messages`, { body, ...(idempotencyKey ? { idempotencyKey } : {}) });
|
|
138
138
|
return formatResult(result);
|
|
139
139
|
}));
|
|
140
|
-
strictInput(server.tool("conduyt_reply_chat_message", "Reply in a Team Chat thread as the API key's user (POST /api/v1/chat/channels/{id}/messages with parentId). parentId must name a live top-level message in that channel. The caller must be a member with communications:create. AI-agent keys are subject to the same six-consecutive-agent-turn cap as a top-level send, and to the same 409 agent_human_required / agents_muted_in_channel refusals (nothing written, do not retry; agents_muted_in_channel = an admin turned \"agents respond here\" off for the channel).", {
|
|
140
|
+
strictInput(server.tool("conduyt_reply_chat_message", "Reply in a Team Chat thread as the API key's user (POST /api/v1/chat/channels/{id}/messages with parentId). parentId must name a live top-level message in that channel. The caller must be a member with communications:create. AI-agent keys are subject to the same six-consecutive-agent-turn cap as a top-level send, and to the same 409 agent_human_required / agents_muted_in_channel refusals (nothing written, do not retry; agents_muted_in_channel = an admin turned \"agents respond here\" off for the channel). (#1213) AI teammates may talk to each other in a thread, bounded: with two consecutive AI posts already (the asker and the answerer), a turn answering an AI teammate's message is refused 409 agent_human_required with reason agent_chain_cap (the same one pause line; the thread waits for a person). A 409 parent_deleted means the parent, its thread or the turn's message was deleted: nothing was written and the turn ended; do not retry. (#1213) While an AI teammate is answering a turn (it holds a running claim) every post must be that turn's reply (its reply:<source> key and claim token, in the source's channel and thread): anything else is refused 409 write_outside_turn, nothing posted. A Grok Bot turn's answer (idempotencyKey turn:<message id>) needs turnToken (409 turn_token_invalid otherwise).", {
|
|
141
141
|
channelId: z.string().uuid().describe("Channel UUID"),
|
|
142
142
|
parentId: z.string().uuid().describe("Top-level message UUID to reply to"),
|
|
143
143
|
body: z.string().trim().min(1).max(10_000).describe("Reply text"),
|
|
144
144
|
idempotencyKey: z.string().regex(/^[A-Za-z0-9:_.-]{1,128}$/).optional().describe("Paired AI agent keys only: a key unique per agent and source turn (the listener uses reply:<source message id>). The same key returns the reply already created (200, idempotentReplay: true) instead of posting a second one; a key used in another channel is a 409 idempotency_key_reused. A person's key gets 400."),
|
|
145
145
|
alsoInChannel: z.boolean().optional().describe("(#684, a person's reply, only while the workspace's thread view is on) true = \"Also send to #channel\": the ONE reply is also shown in the channel (topLevel reads list it with threadReply.reason alsoInChannel); never a second message or notification. Sent only when true. 409 team_chat_threads_off while the thread view is off (nothing posted); an AI teammate never sets it (its first answer already shows in the channel): refused here with claimToken, 400 from the server otherwise"),
|
|
146
146
|
claimToken: z.string().regex(/^[1-9][0-9]{0,8}:[A-Za-z0-9:_.-]{8,128}$/).optional().describe("Paired AI agent keys only; REQUIRED with idempotencyKey reply:<source message id> (refused otherwise, nothing posted): the listener's claim token <generation>:<listener id> from the turn prompt. The reply completes that turn only when posted in the source's channel and thread with the current generation's token; a reply:<source> reply anywhere else, or with a stale token, is refused 409 (reply_wrong_channel / reply_wrong_thread / reply_stale_claim) with nothing posted, so a corrected retry still completes the turn."),
|
|
147
|
-
|
|
147
|
+
turnToken: z.string().regex(/^[1-9][0-9]{9,15}\.[0-9a-f]{64}$/).optional().describe("(#1213) an AI teammate answering a Grok Bot turn: the turn's turnToken from its payload; REQUIRED with idempotencyKey turn:<message id> (refused otherwise, nothing posted)"),
|
|
148
|
+
}, async ({ channelId, parentId, body, idempotencyKey, claimToken, turnToken, alsoInChannel }) => {
|
|
149
|
+
// (#1213) a Grok Bot turn's answer always carries the turn's server-minted token
|
|
150
|
+
if (!turnToken && typeof idempotencyKey === "string" && /^turn:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(idempotencyKey)) {
|
|
151
|
+
throw new Error("turnToken is required with an idempotencyKey turn:<message id> (the turn's turnToken from its payload); nothing was posted");
|
|
152
|
+
}
|
|
148
153
|
// (#599 r7) a reply to an inbox turn (idempotencyKey reply:<source message id>) always names its claim
|
|
149
154
|
if (!claimToken && typeof idempotencyKey === "string" && /^reply:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(idempotencyKey)) {
|
|
150
155
|
throw new Error("claimToken is required with an idempotencyKey reply:<source message id> (the turn prompt's reply claim token); nothing was posted");
|
|
@@ -155,7 +160,7 @@ export function registerChatTools(server, client) {
|
|
|
155
160
|
const also = alsoInChannel === true;
|
|
156
161
|
let result;
|
|
157
162
|
try {
|
|
158
|
-
result = await client.post(`/api/v1/chat/channels/${channelId}/messages`, { body, parentId, ...(idempotencyKey ? { idempotencyKey } : {}), ...(claimToken ? { claimToken } : {}), ...(also ? { alsoInChannel: true } : {}) });
|
|
163
|
+
result = await client.post(`/api/v1/chat/channels/${channelId}/messages`, { body, parentId, ...(idempotencyKey ? { idempotencyKey } : {}), ...(claimToken ? { claimToken } : {}), ...(turnToken ? { turnToken } : {}), ...(also ? { alsoInChannel: true } : {}) });
|
|
159
164
|
}
|
|
160
165
|
catch (err) {
|
|
161
166
|
const refused = also ? threadsRefusal(err, { offSentence: `${THREADS_OFF_SENTENCE} Nothing was posted: send it again without alsoInChannel.` }) : null;
|
|
@@ -167,20 +172,22 @@ export function registerChatTools(server, client) {
|
|
|
167
172
|
const out = formatResult(result);
|
|
168
173
|
return note ? { ...out, content: [...out.content, { type: "text", text: note }] } : out;
|
|
169
174
|
}));
|
|
170
|
-
strictInput(server.tool("conduyt_react_chat_message", "Add a reaction to a Team Chat message as the API key's user (POST /api/v1/chat/channels/{id}/messages/{messageId}/reactions). The server accepts only its Team Chat reaction set and returns the fully hydrated message. Repeating the same reaction is safe and leaves one reaction from this user. (#802, app #655 c) An AI teammate ENDS its turn with a reaction by passing claimToken (the turn prompt's reply claim token): the reaction then completes that turn (outcome reacted) in the same transaction, so the turn needs no reply. React to the turn's message or one in its thread; when it is another message of the thread, pass the turn's message id as sourceMessageId. Exactly one of a reply, a reaction or conduyt_stay_silent ends a turn: a token that is not the running holder's, or a message outside the turn's thread, is refused 409 reaction_stale_claim, and a turn that already ended (a reply, a silence, another reaction) 409 turn_finished with its outcome; nothing is written on either, do not retry. A person's key sending claimToken gets 400.", {
|
|
175
|
+
strictInput(server.tool("conduyt_react_chat_message", "Add a reaction to a Team Chat message as the API key's user (POST /api/v1/chat/channels/{id}/messages/{messageId}/reactions). The server accepts only its Team Chat reaction set and returns the fully hydrated message. Repeating the same reaction is safe and leaves one reaction from this user. (#802, app #655 c) An AI teammate ENDS its turn with a reaction by passing claimToken (the turn prompt's reply claim token): the reaction then completes that turn (outcome reacted) in the same transaction, so the turn needs no reply. React to the turn's message or one in its thread; when it is another message of the thread, pass the turn's message id as sourceMessageId. Exactly one of a reply, a reaction or conduyt_stay_silent ends a turn: a token that is not the running holder's, or a message outside the turn's thread, is refused 409 reaction_stale_claim, and a turn that already ended (a reply, a silence, another reaction) 409 turn_finished with its outcome; nothing is written on either, do not retry. A person's key sending claimToken gets 400. (#1213 r3) While the teammate is answering a turn its only reaction is that turn's (409 write_outside_turn otherwise); a Grok Bot teammate passes its turn's turnToken (and sourceMessageId for another message of the turn's thread).", {
|
|
171
176
|
channelId: z.string().uuid().describe("Channel UUID"),
|
|
172
177
|
messageId: z.string().uuid().describe("Message UUID"),
|
|
173
178
|
emoji: z.string().min(1).max(16).describe("One allowed Team Chat emoji"),
|
|
174
179
|
claimToken: z.string().regex(/^[1-9][0-9]{0,8}:[A-Za-z0-9:_.-]{8,128}$/).optional().describe("Paired AI agent keys only: the turn prompt's reply claim token <generation>:<listener id>; the reaction then ends that turn"),
|
|
175
|
-
sourceMessageId: z.string().uuid().optional().describe("With claimToken
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
+
sourceMessageId: z.string().uuid().optional().describe("With claimToken (or turnToken): the turn's message id, when the reaction targets ANOTHER message of its thread (default: the reacted message)"),
|
|
181
|
+
turnToken: z.string().regex(/^[1-9][0-9]{9,15}\.[0-9a-f]{64}$/).optional().describe("(#1213 r3) an AI teammate answering a Grok Bot turn: the turn's turnToken from its payload; REQUIRED for that teammate's reaction"),
|
|
182
|
+
}, async ({ channelId, messageId, emoji, claimToken, sourceMessageId, turnToken }) => {
|
|
183
|
+
if (sourceMessageId !== undefined && claimToken === undefined && turnToken === undefined)
|
|
184
|
+
throw new Error("sourceMessageId needs claimToken or turnToken (it names the turn the reaction belongs to); nothing was sent");
|
|
185
|
+
const source = sourceMessageId && sourceMessageId.toLowerCase() !== messageId.toLowerCase() ? { sourceMessageId } : {};
|
|
186
|
+
const turn = claimToken ? { claimToken, ...source } : turnToken ? { turnToken, ...source } : {};
|
|
180
187
|
const result = await client.post(`/api/v1/chat/channels/${channelId}/messages/${messageId}/reactions`, { emoji, ...turn });
|
|
181
188
|
return formatResult(result);
|
|
182
189
|
}));
|
|
183
|
-
strictInput(server.tool("conduyt_stay_silent", "(#802, app #655 c; paired AI agent keys) The turn prompt's stay_silent: END an AI teammate's Team Chat turn without replying or reacting, when no response is warranted (POST /api/v1/chat/agents/me/inbox/claim with action silent, for the turn's message, with the generation and listener id read from claimToken). Completes the claim with outcome silent and moves the agent's inbox cursor as an acknowledgement does; repeating it on the claim it completed answers the same ({ silent: true, outcome, cursor }). Exactly one of a reply, a reaction or a silence ends a turn: a turn that already ended with a reply or a reaction is refused 409 turn_finished (outcome says which), a stale token 409 agent_claim_lost; nothing changes on either, do not retry. Any other key is refused.", {
|
|
190
|
+
strictInput(server.tool("conduyt_stay_silent", "(#802, app #655 c; paired AI agent keys) The turn prompt's stay_silent: END an AI teammate's Team Chat turn without replying or reacting, when no response is warranted (POST /api/v1/chat/agents/me/inbox/claim with action silent, for the turn's message, with the generation and listener id read from claimToken). Completes the claim with outcome silent and moves the agent's inbox cursor as an acknowledgement does; repeating it on the claim it completed answers the same ({ silent: true, outcome, cursor }). Exactly one of a reply, a reaction or a silence ends a turn: a turn that already ended with a reply or a reaction is refused 409 turn_finished (outcome says which; (#1213) also paused or trigger_deleted when the server ended it), a stale token 409 agent_claim_lost; nothing changes on either, do not retry. (#1213) Silence is the exception: use it only when the message is clearly not for this teammate; a turn whose teammate already posted in the channel since it started ends replied, never silent. Any other key is refused.", {
|
|
184
191
|
messageId: z.string().uuid().describe("The turn's message id (the turn prompt's Message id)"),
|
|
185
192
|
claimToken: z.string().regex(/^[1-9][0-9]{0,8}:[A-Za-z0-9:_.-]{8,128}$/).describe("The turn prompt's reply claim token <generation>:<listener id>"),
|
|
186
193
|
}, async ({ messageId, claimToken }) => {
|
package/package.json
CHANGED
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
{
|
|
154
154
|
"match": "/api/v1/chat/agents",
|
|
155
155
|
"status": "excluded",
|
|
156
|
-
"reason": "(#599) session-only (assertSessionOnly): an admin manages
|
|
156
|
+
"reason": "(#599) creating, removing and pairing an AI teammate are session-only (assertSessionOnly): an admin manages them in Settings > AI; every API key is refused. (#1153) GET /chat/agents is covered by conduyt_list_ai_teammates and the Grok Bot connect routes by their five tools (a covered endpoint is never matched here)"
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
"match": "/api/v1/chat",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"generatedAt": "2026-11-
|
|
3
|
-
"toolCount":
|
|
4
|
-
"totalEndpoints":
|
|
2
|
+
"generatedAt": "2026-11-08T09:31:39.733Z",
|
|
3
|
+
"toolCount": 393,
|
|
4
|
+
"totalEndpoints": 1168,
|
|
5
5
|
"counts": {
|
|
6
|
-
"covered":
|
|
7
|
-
"excluded":
|
|
8
|
-
"planned":
|
|
6
|
+
"covered": 347,
|
|
7
|
+
"excluded": 360,
|
|
8
|
+
"planned": 461,
|
|
9
9
|
"uncategorized": 0
|
|
10
10
|
},
|
|
11
11
|
"matrix": {
|
|
@@ -365,10 +365,26 @@
|
|
|
365
365
|
"status": "excluded",
|
|
366
366
|
"reason": "super-admin platform operations (Conduyt staff only)"
|
|
367
367
|
},
|
|
368
|
+
"POST /api/v1/admin/external-deletions/:id/confirm-mailbox": {
|
|
369
|
+
"status": "excluded",
|
|
370
|
+
"reason": "super-admin platform operations (Conduyt staff only)"
|
|
371
|
+
},
|
|
372
|
+
"POST /api/v1/admin/external-deletions/:id/release": {
|
|
373
|
+
"status": "excluded",
|
|
374
|
+
"reason": "super-admin platform operations (Conduyt staff only)"
|
|
375
|
+
},
|
|
368
376
|
"POST /api/v1/admin/external-deletions/:id/replay": {
|
|
369
377
|
"status": "excluded",
|
|
370
378
|
"reason": "super-admin platform operations (Conduyt staff only)"
|
|
371
379
|
},
|
|
380
|
+
"POST /api/v1/admin/external-deletions/:id/resolve": {
|
|
381
|
+
"status": "excluded",
|
|
382
|
+
"reason": "super-admin platform operations (Conduyt staff only)"
|
|
383
|
+
},
|
|
384
|
+
"GET /api/v1/admin/external-deletions/recovery": {
|
|
385
|
+
"status": "excluded",
|
|
386
|
+
"reason": "super-admin platform operations (Conduyt staff only)"
|
|
387
|
+
},
|
|
372
388
|
"POST /api/v1/admin/impersonate": {
|
|
373
389
|
"status": "excluded",
|
|
374
390
|
"reason": "super-admin platform operations (Conduyt staff only)"
|
|
@@ -1293,6 +1309,10 @@
|
|
|
1293
1309
|
"status": "planned",
|
|
1294
1310
|
"reason": "call subroutes beyond list/log"
|
|
1295
1311
|
},
|
|
1312
|
+
"POST /api/v1/calls/:id/diagnostics": {
|
|
1313
|
+
"status": "planned",
|
|
1314
|
+
"reason": "call subroutes beyond list/log"
|
|
1315
|
+
},
|
|
1296
1316
|
"POST /api/v1/calls/:id/link": {
|
|
1297
1317
|
"status": "covered",
|
|
1298
1318
|
"tools": [
|
|
@@ -1312,20 +1332,22 @@
|
|
|
1312
1332
|
]
|
|
1313
1333
|
},
|
|
1314
1334
|
"GET /api/v1/chat/agents": {
|
|
1315
|
-
"status": "
|
|
1316
|
-
"
|
|
1335
|
+
"status": "covered",
|
|
1336
|
+
"tools": [
|
|
1337
|
+
"conduyt_list_ai_teammates"
|
|
1338
|
+
]
|
|
1317
1339
|
},
|
|
1318
1340
|
"POST /api/v1/chat/agents": {
|
|
1319
1341
|
"status": "excluded",
|
|
1320
|
-
"reason": "(#599) session-only (assertSessionOnly): an admin manages
|
|
1342
|
+
"reason": "(#599) creating, removing and pairing an AI teammate are session-only (assertSessionOnly): an admin manages them in Settings > AI; every API key is refused. (#1153) GET /chat/agents is covered by conduyt_list_ai_teammates and the Grok Bot connect routes by their five tools (a covered endpoint is never matched here)"
|
|
1321
1343
|
},
|
|
1322
1344
|
"PATCH /api/v1/chat/agents/:id": {
|
|
1323
1345
|
"status": "excluded",
|
|
1324
|
-
"reason": "(#599) session-only (assertSessionOnly): an admin manages
|
|
1346
|
+
"reason": "(#599) creating, removing and pairing an AI teammate are session-only (assertSessionOnly): an admin manages them in Settings > AI; every API key is refused. (#1153) GET /chat/agents is covered by conduyt_list_ai_teammates and the Grok Bot connect routes by their five tools (a covered endpoint is never matched here)"
|
|
1325
1347
|
},
|
|
1326
1348
|
"DELETE /api/v1/chat/agents/:id": {
|
|
1327
1349
|
"status": "excluded",
|
|
1328
|
-
"reason": "(#599) session-only (assertSessionOnly): an admin manages
|
|
1350
|
+
"reason": "(#599) creating, removing and pairing an AI teammate are session-only (assertSessionOnly): an admin manages them in Settings > AI; every API key is refused. (#1153) GET /chat/agents is covered by conduyt_list_ai_teammates and the Grok Bot connect routes by their five tools (a covered endpoint is never matched here)"
|
|
1329
1351
|
},
|
|
1330
1352
|
"POST /api/v1/chat/agents/:id/connect": {
|
|
1331
1353
|
"status": "covered",
|
|
@@ -1335,7 +1357,7 @@
|
|
|
1335
1357
|
},
|
|
1336
1358
|
"POST /api/v1/chat/agents/:id/pair-code": {
|
|
1337
1359
|
"status": "excluded",
|
|
1338
|
-
"reason": "(#599) session-only (assertSessionOnly): an admin manages
|
|
1360
|
+
"reason": "(#599) creating, removing and pairing an AI teammate are session-only (assertSessionOnly): an admin manages them in Settings > AI; every API key is refused. (#1153) GET /chat/agents is covered by conduyt_list_ai_teammates and the Grok Bot connect routes by their five tools (a covered endpoint is never matched here)"
|
|
1339
1361
|
},
|
|
1340
1362
|
"GET /api/v1/chat/agents/:id/webhook": {
|
|
1341
1363
|
"status": "covered",
|
|
@@ -1379,6 +1401,10 @@
|
|
|
1379
1401
|
"conduyt_stay_silent"
|
|
1380
1402
|
]
|
|
1381
1403
|
},
|
|
1404
|
+
"PUT /api/v1/chat/agents/me/knowledge": {
|
|
1405
|
+
"status": "excluded",
|
|
1406
|
+
"reason": "(#599) the listener protocol of a paired agent key (long poll, per-event ack, heartbeat): served by the npm CLI `conduyt agent listen`, not a request/response MCP tool"
|
|
1407
|
+
},
|
|
1382
1408
|
"GET /api/v1/chat/agents/me/mcp": {
|
|
1383
1409
|
"status": "excluded",
|
|
1384
1410
|
"reason": "(#599) the listener protocol of a paired agent key (long poll, per-event ack, heartbeat): served by the npm CLI `conduyt agent listen`, not a request/response MCP tool"
|
|
@@ -1467,6 +1493,14 @@
|
|
|
1467
1493
|
"conduyt_list_chat_thread_files"
|
|
1468
1494
|
]
|
|
1469
1495
|
},
|
|
1496
|
+
"PUT /api/v1/chat/channels/:id/messages/:messageId/follow": {
|
|
1497
|
+
"status": "planned",
|
|
1498
|
+
"reason": "team chat read/post for agents"
|
|
1499
|
+
},
|
|
1500
|
+
"DELETE /api/v1/chat/channels/:id/messages/:messageId/follow": {
|
|
1501
|
+
"status": "planned",
|
|
1502
|
+
"reason": "team chat read/post for agents"
|
|
1503
|
+
},
|
|
1470
1504
|
"POST /api/v1/chat/channels/:id/messages/:messageId/handoff": {
|
|
1471
1505
|
"status": "planned",
|
|
1472
1506
|
"reason": "team chat read/post for agents"
|