@todoist/comms-mcp 0.0.1 → 11.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +198 -34
- package/dist/comms-tool.d.ts +14 -0
- package/dist/comms-tool.js +1 -0
- package/dist/index.d.ts +1324 -0
- package/dist/index.js +55 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +24 -0
- package/dist/mcp-helpers.d.ts +17 -0
- package/dist/mcp-helpers.js +40 -0
- package/dist/mcp-server.d.ts +10 -0
- package/dist/mcp-server.js +78 -0
- package/dist/tool-registry.d.ts +3 -0
- package/dist/tool-registry.js +49 -0
- package/dist/tools/build-link.d.ts +63 -0
- package/dist/tools/build-link.js +93 -0
- package/dist/tools/channel-output.d.ts +19 -0
- package/dist/tools/channel-output.js +47 -0
- package/dist/tools/create-channel.d.ts +55 -0
- package/dist/tools/create-channel.js +44 -0
- package/dist/tools/create-conversation.d.ts +49 -0
- package/dist/tools/create-conversation.js +75 -0
- package/dist/tools/create-thread.d.ts +67 -0
- package/dist/tools/create-thread.js +126 -0
- package/dist/tools/delete-object.d.ts +44 -0
- package/dist/tools/delete-object.js +84 -0
- package/dist/tools/fetch-inbox.d.ts +91 -0
- package/dist/tools/fetch-inbox.js +227 -0
- package/dist/tools/get-groups.d.ts +62 -0
- package/dist/tools/get-groups.js +116 -0
- package/dist/tools/get-mentions.d.ts +97 -0
- package/dist/tools/get-mentions.js +126 -0
- package/dist/tools/get-users.d.ts +58 -0
- package/dist/tools/get-users.js +107 -0
- package/dist/tools/get-workspaces.d.ts +61 -0
- package/dist/tools/get-workspaces.js +120 -0
- package/dist/tools/list-channels.d.ts +47 -0
- package/dist/tools/list-channels.js +89 -0
- package/dist/tools/list-conversations.d.ts +66 -0
- package/dist/tools/list-conversations.js +212 -0
- package/dist/tools/load-conversation.d.ts +92 -0
- package/dist/tools/load-conversation.js +128 -0
- package/dist/tools/load-thread.d.ts +127 -0
- package/dist/tools/load-thread.js +175 -0
- package/dist/tools/mark-done.d.ts +111 -0
- package/dist/tools/mark-done.js +275 -0
- package/dist/tools/mark-read.d.ts +73 -0
- package/dist/tools/mark-read.js +214 -0
- package/dist/tools/react.d.ts +59 -0
- package/dist/tools/react.js +97 -0
- package/dist/tools/reply.d.ts +69 -0
- package/dist/tools/reply.js +122 -0
- package/dist/tools/search-content.d.ts +103 -0
- package/dist/tools/search-content.js +138 -0
- package/dist/tools/update-channel.d.ts +53 -0
- package/dist/tools/update-channel.js +45 -0
- package/dist/tools/update-object.d.ts +52 -0
- package/dist/tools/update-object.js +161 -0
- package/dist/tools/update-user-info.d.ts +55 -0
- package/dist/tools/update-user-info.js +270 -0
- package/dist/tools/user-info.d.ts +32 -0
- package/dist/tools/user-info.js +16 -0
- package/dist/tools/user-output.d.ts +9 -0
- package/dist/tools/user-output.js +28 -0
- package/dist/usage-tracking.d.ts +23 -0
- package/dist/usage-tracking.js +41 -0
- package/dist/utils/attachments.d.ts +24 -0
- package/dist/utils/attachments.js +57 -0
- package/dist/utils/concurrency.d.ts +20 -0
- package/dist/utils/concurrency.js +24 -0
- package/dist/utils/constants.d.ts +5 -0
- package/dist/utils/constants.js +5 -0
- package/dist/utils/degrade.d.ts +81 -0
- package/dist/utils/degrade.js +149 -0
- package/dist/utils/output-schemas.d.ts +1283 -0
- package/dist/utils/output-schemas.js +657 -0
- package/dist/utils/read-position.d.ts +21 -0
- package/dist/utils/read-position.js +37 -0
- package/dist/utils/search-results.d.ts +36 -0
- package/dist/utils/search-results.js +82 -0
- package/dist/utils/target-types.d.ts +114 -0
- package/dist/utils/target-types.js +58 -0
- package/dist/utils/test-helpers.d.ts +95 -0
- package/dist/utils/test-helpers.js +231 -0
- package/dist/utils/tool-names.d.ts +25 -0
- package/dist/utils/tool-names.js +25 -0
- package/dist/utils/url-helpers.d.ts +3 -0
- package/dist/utils/url-helpers.js +10 -0
- package/dist/utils/user-status.d.ts +8 -0
- package/dist/utils/user-status.js +33 -0
- package/dist/utils/validate-comms-token.d.ts +13 -0
- package/dist/utils/validate-comms-token.js +22 -0
- package/package.json +85 -8
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { limitedAll } from '../utils/concurrency.js';
|
|
3
|
+
import { logOperationFailures, SAMPLE_LIMIT } from '../utils/degrade.js';
|
|
4
|
+
import { MarkReadOutputSchema } from '../utils/output-schemas.js';
|
|
5
|
+
import { markConversationFullyRead, markThreadFullyRead } from '../utils/read-position.js';
|
|
6
|
+
import { ToolNames } from '../utils/tool-names.js';
|
|
7
|
+
const ArgsSchema = {
|
|
8
|
+
workspaceId: z
|
|
9
|
+
.number()
|
|
10
|
+
.describe('The workspace the threads and conversations belong to. Its unread lists decide which requested items actually need marking.'),
|
|
11
|
+
threadIds: z
|
|
12
|
+
.array(z.string())
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('Thread IDs to mark as read. Threads that are not currently unread are reported as alreadyRead and left untouched.'),
|
|
15
|
+
conversationIds: z
|
|
16
|
+
.array(z.string())
|
|
17
|
+
.optional()
|
|
18
|
+
.describe('Conversation IDs to mark as read. Conversations that are not currently unread are reported as alreadyRead and left untouched.'),
|
|
19
|
+
all: z
|
|
20
|
+
.boolean()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('Mark every unread thread and conversation in the workspace as read. Cannot be combined with threadIds or conversationIds.'),
|
|
23
|
+
};
|
|
24
|
+
function dedupe(ids) {
|
|
25
|
+
return [...new Set(ids ?? [])];
|
|
26
|
+
}
|
|
27
|
+
function errorMessage(error) {
|
|
28
|
+
return error instanceof Error ? error.message : 'Unknown error';
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Marks each item read through its latest comment/message, one API pair per
|
|
32
|
+
* item. Failures are returned per item rather than thrown so one deleted or
|
|
33
|
+
* inaccessible item never stops the rest of the batch.
|
|
34
|
+
*/
|
|
35
|
+
async function markEach(client, itemType, ids) {
|
|
36
|
+
const markOne = itemType === 'thread' ? markThreadFullyRead : markConversationFullyRead;
|
|
37
|
+
const results = await limitedAll(ids, async (id) => {
|
|
38
|
+
try {
|
|
39
|
+
await markOne(client, id);
|
|
40
|
+
return { id };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return { id, error: errorMessage(error) };
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
const marked = [];
|
|
47
|
+
const failed = [];
|
|
48
|
+
for (const result of results) {
|
|
49
|
+
if (result.error === undefined) {
|
|
50
|
+
marked.push(result.id);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
failed.push({ item: result.id, itemType, error: result.error });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { marked, failed };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Marks every unread thread in the workspace read with a single call. The
|
|
60
|
+
* IDs come from the unread list fetched beforehand, so the result can still
|
|
61
|
+
* name what was marked; if the call fails, every one of them is reported.
|
|
62
|
+
*/
|
|
63
|
+
async function markAllThreads(client, workspaceId, unreadThreadIds) {
|
|
64
|
+
if (unreadThreadIds.length === 0) {
|
|
65
|
+
return { marked: [], failed: [] };
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
await client.threads.markAllRead({ workspaceId });
|
|
69
|
+
return { marked: [...unreadThreadIds], failed: [] };
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
const message = errorMessage(error);
|
|
73
|
+
return {
|
|
74
|
+
marked: [],
|
|
75
|
+
failed: unreadThreadIds.map((item) => ({ item, itemType: 'thread', error: message })),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Lists IDs for the text output. In `all` mode the caller never supplied the
|
|
81
|
+
* IDs and cannot act on them individually, so a workspace with hundreds of
|
|
82
|
+
* unread items would only flood the context; the list is capped there while
|
|
83
|
+
* `structuredContent` keeps every ID.
|
|
84
|
+
*/
|
|
85
|
+
function formatIdList(ids, cap) {
|
|
86
|
+
if (ids.length === 0) {
|
|
87
|
+
return 'none';
|
|
88
|
+
}
|
|
89
|
+
if (cap === null || ids.length <= cap) {
|
|
90
|
+
return ids.join(', ');
|
|
91
|
+
}
|
|
92
|
+
return `${ids.slice(0, cap).join(', ')} … and ${ids.length - cap} more`;
|
|
93
|
+
}
|
|
94
|
+
function formatItemSection(label, items, mode) {
|
|
95
|
+
const cap = mode === 'all' ? SAMPLE_LIMIT : null;
|
|
96
|
+
const lines = [`## ${label}`, ''];
|
|
97
|
+
lines.push(`**Marked:** ${formatIdList(items.marked, cap)}`);
|
|
98
|
+
if (mode === 'individual') {
|
|
99
|
+
lines.push(`**Already read:** ${formatIdList(items.alreadyRead, cap)}`);
|
|
100
|
+
}
|
|
101
|
+
lines.push('');
|
|
102
|
+
return lines;
|
|
103
|
+
}
|
|
104
|
+
const markRead = {
|
|
105
|
+
name: ToolNames.MARK_READ,
|
|
106
|
+
description: 'Mark threads and/or conversations as read without archiving them, so they stay in the inbox but no longer count as unread. Pass specific threadIds and/or conversationIds, or all: true to mark everything unread in the workspace. Items that are not currently unread are skipped and reported as alreadyRead. Use mark-done instead when the items should also be archived.',
|
|
107
|
+
parameters: ArgsSchema,
|
|
108
|
+
outputSchema: MarkReadOutputSchema.shape,
|
|
109
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
110
|
+
async execute(args, client) {
|
|
111
|
+
const { workspaceId, all = false } = args;
|
|
112
|
+
const threadIds = dedupe(args.threadIds);
|
|
113
|
+
const conversationIds = dedupe(args.conversationIds);
|
|
114
|
+
if (all && (threadIds.length > 0 || conversationIds.length > 0)) {
|
|
115
|
+
throw new Error('`all` cannot be combined with threadIds or conversationIds');
|
|
116
|
+
}
|
|
117
|
+
if (!all && threadIds.length === 0 && conversationIds.length === 0) {
|
|
118
|
+
throw new Error('Provide threadIds, conversationIds, or all: true');
|
|
119
|
+
}
|
|
120
|
+
const mode = all ? 'all' : 'individual';
|
|
121
|
+
// The unread lists are the source of truth for what needs marking. With
|
|
122
|
+
// explicit IDs they let already-read items be skipped (and reported as
|
|
123
|
+
// such) instead of re-marked; with `all` they are the work list itself.
|
|
124
|
+
// Each list is only fetched when that item type is in play.
|
|
125
|
+
const [unreadThreads, unreadConversations] = await Promise.all([
|
|
126
|
+
all || threadIds.length > 0
|
|
127
|
+
? client.threads.getUnread(workspaceId).then((r) => r.data.map((t) => t.threadId))
|
|
128
|
+
: [],
|
|
129
|
+
all || conversationIds.length > 0
|
|
130
|
+
? client.conversations
|
|
131
|
+
.getUnread(workspaceId)
|
|
132
|
+
.then((r) => r.data.map((c) => c.conversationId))
|
|
133
|
+
: [],
|
|
134
|
+
]);
|
|
135
|
+
const threads = { marked: [], alreadyRead: [] };
|
|
136
|
+
const conversations = { marked: [], alreadyRead: [] };
|
|
137
|
+
const failed = [];
|
|
138
|
+
if (all) {
|
|
139
|
+
// One workspace-level call covers every unread thread. There is no
|
|
140
|
+
// working equivalent for conversations, so those go one by one,
|
|
141
|
+
// concurrently with the thread call.
|
|
142
|
+
const [threadResult, conversationResult] = await Promise.all([
|
|
143
|
+
markAllThreads(client, workspaceId, unreadThreads),
|
|
144
|
+
markEach(client, 'conversation', unreadConversations),
|
|
145
|
+
]);
|
|
146
|
+
threads.marked = threadResult.marked;
|
|
147
|
+
conversations.marked = conversationResult.marked;
|
|
148
|
+
failed.push(...threadResult.failed, ...conversationResult.failed);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
const unreadThreadSet = new Set(unreadThreads);
|
|
152
|
+
const unreadConversationSet = new Set(unreadConversations);
|
|
153
|
+
threads.alreadyRead = threadIds.filter((id) => !unreadThreadSet.has(id));
|
|
154
|
+
conversations.alreadyRead = conversationIds.filter((id) => !unreadConversationSet.has(id));
|
|
155
|
+
const [threadResult, conversationResult] = await Promise.all([
|
|
156
|
+
markEach(client, 'thread', threadIds.filter((id) => unreadThreadSet.has(id))),
|
|
157
|
+
markEach(client, 'conversation', conversationIds.filter((id) => unreadConversationSet.has(id))),
|
|
158
|
+
]);
|
|
159
|
+
threads.marked = threadResult.marked;
|
|
160
|
+
conversations.marked = conversationResult.marked;
|
|
161
|
+
failed.push(...threadResult.failed, ...conversationResult.failed);
|
|
162
|
+
}
|
|
163
|
+
logOperationFailures(ToolNames.MARK_READ, failed);
|
|
164
|
+
const markedCount = threads.marked.length + conversations.marked.length;
|
|
165
|
+
const alreadyReadCount = threads.alreadyRead.length + conversations.alreadyRead.length;
|
|
166
|
+
const lines = ['# Mark Read', ''];
|
|
167
|
+
lines.push(`**Workspace ID:** ${workspaceId}`);
|
|
168
|
+
lines.push(`**Mode:** ${mode === 'all' ? 'All unread in workspace' : 'Individual IDs'}`);
|
|
169
|
+
lines.push(`**Marked:** ${markedCount}`);
|
|
170
|
+
if (mode === 'individual') {
|
|
171
|
+
lines.push(`**Already read:** ${alreadyReadCount}`);
|
|
172
|
+
}
|
|
173
|
+
lines.push(`**Failed:** ${failed.length}`);
|
|
174
|
+
lines.push('');
|
|
175
|
+
if (mode === 'all' || threadIds.length > 0) {
|
|
176
|
+
lines.push(...formatItemSection('Threads', threads, mode));
|
|
177
|
+
}
|
|
178
|
+
if (mode === 'all' || conversationIds.length > 0) {
|
|
179
|
+
lines.push(...formatItemSection('Conversations', conversations, mode));
|
|
180
|
+
}
|
|
181
|
+
if (failed.length > 0) {
|
|
182
|
+
lines.push('## Failed', '');
|
|
183
|
+
for (const failure of failed) {
|
|
184
|
+
lines.push(`- ${failure.itemType} ${failure.item}: ${failure.error}`);
|
|
185
|
+
}
|
|
186
|
+
lines.push('');
|
|
187
|
+
}
|
|
188
|
+
lines.push('## Next Steps', '');
|
|
189
|
+
if (failed.length > 0) {
|
|
190
|
+
lines.push('Review failed items and retry if needed.');
|
|
191
|
+
}
|
|
192
|
+
else if (markedCount === 0) {
|
|
193
|
+
lines.push('Nothing was unread, so no changes were made.');
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
lines.push('Items remain in the inbox. Use `mark-done` to archive them, or `fetch-inbox` with `onlyUnread: true` to see what is still unread.');
|
|
197
|
+
}
|
|
198
|
+
const structuredContent = {
|
|
199
|
+
type: 'mark_read_result',
|
|
200
|
+
workspaceId,
|
|
201
|
+
mode,
|
|
202
|
+
threads,
|
|
203
|
+
conversations,
|
|
204
|
+
failed,
|
|
205
|
+
markedCount,
|
|
206
|
+
failureCount: failed.length,
|
|
207
|
+
};
|
|
208
|
+
return {
|
|
209
|
+
textContent: lines.join('\n'),
|
|
210
|
+
structuredContent,
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
export { markRead };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type ReactionTargetType } from '../utils/target-types.js';
|
|
3
|
+
type ReactStructured = {
|
|
4
|
+
type: 'reaction_result';
|
|
5
|
+
success: boolean;
|
|
6
|
+
operation: 'add' | 'remove';
|
|
7
|
+
targetType: ReactionTargetType;
|
|
8
|
+
targetId: string;
|
|
9
|
+
emoji: string;
|
|
10
|
+
targetUrl: string;
|
|
11
|
+
};
|
|
12
|
+
declare const react: {
|
|
13
|
+
name: "react";
|
|
14
|
+
description: string;
|
|
15
|
+
parameters: {
|
|
16
|
+
targetType: z.ZodEnum<{
|
|
17
|
+
thread: "thread";
|
|
18
|
+
comment: "comment";
|
|
19
|
+
message: "message";
|
|
20
|
+
}>;
|
|
21
|
+
targetId: z.ZodString;
|
|
22
|
+
emoji: z.ZodString;
|
|
23
|
+
operation: z.ZodDefault<z.ZodEnum<{
|
|
24
|
+
add: "add";
|
|
25
|
+
remove: "remove";
|
|
26
|
+
}>>;
|
|
27
|
+
};
|
|
28
|
+
outputSchema: {
|
|
29
|
+
type: z.ZodLiteral<"reaction_result">;
|
|
30
|
+
success: z.ZodBoolean;
|
|
31
|
+
operation: z.ZodEnum<{
|
|
32
|
+
add: "add";
|
|
33
|
+
remove: "remove";
|
|
34
|
+
}>;
|
|
35
|
+
targetType: z.ZodEnum<{
|
|
36
|
+
thread: "thread";
|
|
37
|
+
comment: "comment";
|
|
38
|
+
message: "message";
|
|
39
|
+
}>;
|
|
40
|
+
targetId: z.ZodString;
|
|
41
|
+
emoji: z.ZodString;
|
|
42
|
+
targetUrl: z.ZodString;
|
|
43
|
+
};
|
|
44
|
+
annotations: {
|
|
45
|
+
readOnlyHint: false;
|
|
46
|
+
destructiveHint: true;
|
|
47
|
+
idempotentHint: false;
|
|
48
|
+
};
|
|
49
|
+
execute(args: {
|
|
50
|
+
targetType: "thread" | "comment" | "message";
|
|
51
|
+
targetId: string;
|
|
52
|
+
emoji: string;
|
|
53
|
+
operation: "add" | "remove";
|
|
54
|
+
}, client: import("@doist/comms-sdk").CommsApi): Promise<{
|
|
55
|
+
textContent: string;
|
|
56
|
+
structuredContent: ReactStructured;
|
|
57
|
+
}>;
|
|
58
|
+
};
|
|
59
|
+
export { react, type ReactStructured };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { getFullCommsURL } from '@doist/comms-sdk';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { ReactOutputSchema } from '../utils/output-schemas.js';
|
|
4
|
+
import { ReactionTargetTypeSchema } from '../utils/target-types.js';
|
|
5
|
+
import { ToolNames } from '../utils/tool-names.js';
|
|
6
|
+
const ArgsSchema = {
|
|
7
|
+
targetType: ReactionTargetTypeSchema.describe('The type of object to react to: thread, comment, or message.'),
|
|
8
|
+
targetId: z.string().describe('The ID of the thread, comment, or message to react to.'),
|
|
9
|
+
emoji: z.string().min(1).describe('The emoji to react with (e.g., "👍", "❤️", "🎉").'),
|
|
10
|
+
operation: z
|
|
11
|
+
.enum(['add', 'remove'])
|
|
12
|
+
.default('add')
|
|
13
|
+
.describe('Whether to add or remove the reaction.'),
|
|
14
|
+
};
|
|
15
|
+
const react = {
|
|
16
|
+
name: ToolNames.REACT,
|
|
17
|
+
description: 'Add or remove an emoji reaction on a thread, comment, or conversation message. Use targetType to specify the type of object (thread, comment, or message) and targetId for the ID.',
|
|
18
|
+
parameters: ArgsSchema,
|
|
19
|
+
outputSchema: ReactOutputSchema.shape,
|
|
20
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false },
|
|
21
|
+
async execute(args, client) {
|
|
22
|
+
const { targetType, targetId, emoji, operation } = args;
|
|
23
|
+
let targetUrl;
|
|
24
|
+
// Fetch target metadata to get URL
|
|
25
|
+
if (targetType === 'thread') {
|
|
26
|
+
const thread = await client.threads.getThread(targetId);
|
|
27
|
+
targetUrl =
|
|
28
|
+
thread.url ??
|
|
29
|
+
getFullCommsURL({
|
|
30
|
+
workspaceId: thread.workspaceId,
|
|
31
|
+
channelId: thread.channelId,
|
|
32
|
+
threadId: thread.id,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
else if (targetType === 'comment') {
|
|
36
|
+
const comment = await client.comments.getComment(targetId);
|
|
37
|
+
targetUrl =
|
|
38
|
+
comment.url ??
|
|
39
|
+
getFullCommsURL({
|
|
40
|
+
workspaceId: comment.workspaceId,
|
|
41
|
+
channelId: comment.channelId,
|
|
42
|
+
threadId: comment.threadId,
|
|
43
|
+
commentId: comment.id,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// message
|
|
48
|
+
const message = await client.conversationMessages.getMessage(targetId);
|
|
49
|
+
targetUrl =
|
|
50
|
+
message.url ??
|
|
51
|
+
getFullCommsURL({
|
|
52
|
+
workspaceId: message.workspaceId,
|
|
53
|
+
conversationId: message.conversationId,
|
|
54
|
+
messageId: message.id,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
// Map targetType to the appropriate API parameter
|
|
58
|
+
const apiParams = { reaction: emoji };
|
|
59
|
+
if (targetType === 'thread') {
|
|
60
|
+
apiParams.threadId = targetId;
|
|
61
|
+
}
|
|
62
|
+
else if (targetType === 'comment') {
|
|
63
|
+
apiParams.commentId = targetId;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
apiParams.messageId = targetId;
|
|
67
|
+
}
|
|
68
|
+
// Perform the reaction operation
|
|
69
|
+
if (operation === 'add') {
|
|
70
|
+
await client.reactions.add({ ...apiParams, reaction: emoji });
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
await client.reactions.remove({ ...apiParams, reaction: emoji });
|
|
74
|
+
}
|
|
75
|
+
const lines = [
|
|
76
|
+
`# Reaction ${operation === 'add' ? 'Added' : 'Removed'}`,
|
|
77
|
+
'',
|
|
78
|
+
`**Target:** ${targetType} ${targetId}`,
|
|
79
|
+
`**Emoji:** ${emoji}`,
|
|
80
|
+
`**Operation:** ${operation}`,
|
|
81
|
+
];
|
|
82
|
+
const structuredContent = {
|
|
83
|
+
type: 'reaction_result',
|
|
84
|
+
success: true,
|
|
85
|
+
operation,
|
|
86
|
+
targetType,
|
|
87
|
+
targetId,
|
|
88
|
+
emoji,
|
|
89
|
+
targetUrl,
|
|
90
|
+
};
|
|
91
|
+
return {
|
|
92
|
+
textContent: lines.join('\n'),
|
|
93
|
+
structuredContent,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
export { react };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type ReplyOutput } from '../utils/output-schemas.js';
|
|
3
|
+
declare const reply: {
|
|
4
|
+
name: "reply";
|
|
5
|
+
description: string;
|
|
6
|
+
parameters: {
|
|
7
|
+
targetType: z.ZodEnum<{
|
|
8
|
+
thread: "thread";
|
|
9
|
+
conversation: "conversation";
|
|
10
|
+
}>;
|
|
11
|
+
targetId: z.ZodString;
|
|
12
|
+
content: z.ZodString;
|
|
13
|
+
recipients: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
14
|
+
groups: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
15
|
+
notifyAudience: z.ZodOptional<z.ZodEnum<{
|
|
16
|
+
thread: "thread";
|
|
17
|
+
channel: "channel";
|
|
18
|
+
}>>;
|
|
19
|
+
};
|
|
20
|
+
outputSchema: {
|
|
21
|
+
type: z.ZodLiteral<"reply_result">;
|
|
22
|
+
success: z.ZodBoolean;
|
|
23
|
+
targetType: z.ZodEnum<{
|
|
24
|
+
thread: "thread";
|
|
25
|
+
conversation: "conversation";
|
|
26
|
+
}>;
|
|
27
|
+
targetId: z.ZodString;
|
|
28
|
+
replyId: z.ZodString;
|
|
29
|
+
content: z.ZodString;
|
|
30
|
+
created: z.ZodString;
|
|
31
|
+
replyUrl: z.ZodString;
|
|
32
|
+
recipients: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
33
|
+
notifyAudience: z.ZodOptional<z.ZodEnum<{
|
|
34
|
+
thread: "thread";
|
|
35
|
+
channel: "channel";
|
|
36
|
+
}>>;
|
|
37
|
+
groups: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
38
|
+
};
|
|
39
|
+
annotations: {
|
|
40
|
+
readOnlyHint: false;
|
|
41
|
+
destructiveHint: false;
|
|
42
|
+
idempotentHint: false;
|
|
43
|
+
};
|
|
44
|
+
execute(args: {
|
|
45
|
+
targetType: "thread" | "conversation";
|
|
46
|
+
targetId: string;
|
|
47
|
+
content: string;
|
|
48
|
+
recipients?: number[] | undefined;
|
|
49
|
+
groups?: string[] | undefined;
|
|
50
|
+
notifyAudience?: "thread" | "channel" | undefined;
|
|
51
|
+
}, client: import("@doist/comms-sdk").CommsApi): Promise<{
|
|
52
|
+
textContent: string;
|
|
53
|
+
structuredContent: {
|
|
54
|
+
type: "reply_result";
|
|
55
|
+
success: boolean;
|
|
56
|
+
targetType: "thread" | "conversation";
|
|
57
|
+
targetId: string;
|
|
58
|
+
replyId: string;
|
|
59
|
+
content: string;
|
|
60
|
+
created: string;
|
|
61
|
+
replyUrl: string;
|
|
62
|
+
recipients?: number[] | undefined;
|
|
63
|
+
notifyAudience?: "thread" | "channel" | undefined;
|
|
64
|
+
groups?: string[] | undefined;
|
|
65
|
+
};
|
|
66
|
+
}>;
|
|
67
|
+
};
|
|
68
|
+
type ReplyStructured = ReplyOutput;
|
|
69
|
+
export { reply, type ReplyStructured };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { getFullCommsURL, NOTIFY_AUDIENCES } from '@doist/comms-sdk';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { ReplyOutputSchema } from '../utils/output-schemas.js';
|
|
4
|
+
import { ReplyTargetTypeSchema } from '../utils/target-types.js';
|
|
5
|
+
import { ToolNames } from '../utils/tool-names.js';
|
|
6
|
+
const ArgsSchema = {
|
|
7
|
+
targetType: ReplyTargetTypeSchema.describe('The type of object to reply to: thread (posts a comment) or conversation (posts a message).'),
|
|
8
|
+
targetId: z.string().describe('The ID of the thread or conversation to reply to.'),
|
|
9
|
+
content: z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.describe('The content of the reply. Markdown. Mention people with the link syntax [Name](comms-mention://USER_ID) — e.g. [Afzal](comms-mention://29367677) — never @Name or [[Name|id]], which post as literal text. Do not put "@" in the label; the client adds it. Groups use [Name](comms-group-mention://GROUP_ID), channels [#name](comms-channel://CHANNEL_ID), threads [Title](comms-thread://THREAD_ID). Resolve IDs with get-users/get-groups/list-channels first. Mentioning inline does not notify anyone. For thread replies, also pass mentioned user IDs in recipients and group IDs in groups; conversation replies notify all participants automatically and take no notification parameters.'),
|
|
13
|
+
recipients: z
|
|
14
|
+
.array(z.number())
|
|
15
|
+
.optional()
|
|
16
|
+
.describe('Optional array of user IDs to notify (thread replies only; ignored for conversation replies, which notify all participants automatically). If omitted with no groups and no notifyAudience, thread replies default to notifying everyone who has interacted with the thread.'),
|
|
17
|
+
groups: z
|
|
18
|
+
.array(z.string())
|
|
19
|
+
.optional()
|
|
20
|
+
.describe('Optional array of group IDs to notify (only for thread replies). Use get-groups to discover group IDs before passing them here.'),
|
|
21
|
+
notifyAudience: z
|
|
22
|
+
.enum(NOTIFY_AUDIENCES)
|
|
23
|
+
.optional()
|
|
24
|
+
.describe("Optional broader audience to notify in addition to recipients and groups (only for thread replies). 'channel' notifies everyone in the channel; 'thread' notifies everyone who has interacted with the thread."),
|
|
25
|
+
};
|
|
26
|
+
const reply = {
|
|
27
|
+
name: ToolNames.REPLY,
|
|
28
|
+
description: 'Post a reply to a thread (as a comment) or conversation (as a message). Thread replies notify everyone who has interacted with the thread by default unless specific user recipients, groups, or a notifyAudience are provided.',
|
|
29
|
+
parameters: ArgsSchema,
|
|
30
|
+
outputSchema: ReplyOutputSchema.shape,
|
|
31
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false },
|
|
32
|
+
async execute(args, client) {
|
|
33
|
+
const { targetType, targetId, content, recipients, groups, notifyAudience } = args;
|
|
34
|
+
if (targetType === 'conversation' && groups !== undefined) {
|
|
35
|
+
throw new Error('groups can only be used when replying to a thread.');
|
|
36
|
+
}
|
|
37
|
+
if (targetType === 'conversation' && notifyAudience !== undefined) {
|
|
38
|
+
throw new Error('notifyAudience can only be used when replying to a thread.');
|
|
39
|
+
}
|
|
40
|
+
const groupsToNotify = targetType === 'thread' && groups && groups.length > 0 ? groups : undefined;
|
|
41
|
+
const appliedAudience = targetType === 'thread'
|
|
42
|
+
? (notifyAudience ??
|
|
43
|
+
(recipients === undefined && !groupsToNotify ? 'thread' : undefined))
|
|
44
|
+
: undefined;
|
|
45
|
+
let replyId;
|
|
46
|
+
let created;
|
|
47
|
+
let replyUrl;
|
|
48
|
+
if (targetType === 'thread') {
|
|
49
|
+
const comment = await client.comments.createComment({
|
|
50
|
+
threadId: targetId,
|
|
51
|
+
content,
|
|
52
|
+
recipients,
|
|
53
|
+
groups: groupsToNotify,
|
|
54
|
+
notifyAudience: appliedAudience,
|
|
55
|
+
});
|
|
56
|
+
replyId = comment.id;
|
|
57
|
+
replyUrl =
|
|
58
|
+
comment.url ??
|
|
59
|
+
getFullCommsURL({
|
|
60
|
+
workspaceId: comment.workspaceId,
|
|
61
|
+
channelId: comment.channelId,
|
|
62
|
+
threadId: comment.threadId,
|
|
63
|
+
commentId: comment.id,
|
|
64
|
+
});
|
|
65
|
+
const postedValue = comment.posted;
|
|
66
|
+
created = postedValue
|
|
67
|
+
? typeof postedValue === 'string'
|
|
68
|
+
? new Date(postedValue)
|
|
69
|
+
: postedValue
|
|
70
|
+
: new Date();
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const message = await client.conversationMessages.createMessage({
|
|
74
|
+
conversationId: targetId,
|
|
75
|
+
content,
|
|
76
|
+
});
|
|
77
|
+
replyId = message.id;
|
|
78
|
+
replyUrl =
|
|
79
|
+
message.url ??
|
|
80
|
+
getFullCommsURL({
|
|
81
|
+
workspaceId: message.workspaceId,
|
|
82
|
+
conversationId: message.conversationId,
|
|
83
|
+
messageId: message.id,
|
|
84
|
+
});
|
|
85
|
+
const postedValue = message.posted;
|
|
86
|
+
created = postedValue
|
|
87
|
+
? typeof postedValue === 'string'
|
|
88
|
+
? new Date(postedValue)
|
|
89
|
+
: postedValue
|
|
90
|
+
: new Date();
|
|
91
|
+
}
|
|
92
|
+
const lines = [
|
|
93
|
+
`# Reply Posted`,
|
|
94
|
+
'',
|
|
95
|
+
`**Target:** ${targetType === 'thread' ? `Thread ${targetId}` : `Conversation ${targetId}`}`,
|
|
96
|
+
`**Reply ID:** ${replyId}`,
|
|
97
|
+
`**Created:** ${created.toISOString()}`,
|
|
98
|
+
'',
|
|
99
|
+
'## Content',
|
|
100
|
+
'',
|
|
101
|
+
content,
|
|
102
|
+
];
|
|
103
|
+
const structuredContent = {
|
|
104
|
+
type: 'reply_result',
|
|
105
|
+
success: true,
|
|
106
|
+
targetType,
|
|
107
|
+
targetId,
|
|
108
|
+
replyId,
|
|
109
|
+
content,
|
|
110
|
+
created: created.toISOString(),
|
|
111
|
+
replyUrl,
|
|
112
|
+
...(targetType === 'thread' && recipients ? { recipients } : {}),
|
|
113
|
+
...(groupsToNotify ? { groups: groupsToNotify } : {}),
|
|
114
|
+
...(appliedAudience ? { notifyAudience: appliedAudience } : {}),
|
|
115
|
+
};
|
|
116
|
+
return {
|
|
117
|
+
textContent: lines.join('\n'),
|
|
118
|
+
structuredContent,
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
export { reply };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type SearchContentOutput } from '../utils/output-schemas.js';
|
|
3
|
+
type SearchContentStructured = SearchContentOutput;
|
|
4
|
+
declare const searchContent: {
|
|
5
|
+
name: "search-content";
|
|
6
|
+
description: string;
|
|
7
|
+
parameters: {
|
|
8
|
+
query: z.ZodString;
|
|
9
|
+
workspaceId: z.ZodNumber;
|
|
10
|
+
channelIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
11
|
+
authorIds: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
12
|
+
mentionSelf: z.ZodOptional<z.ZodBoolean>;
|
|
13
|
+
dateFrom: z.ZodOptional<z.ZodString>;
|
|
14
|
+
dateTo: z.ZodOptional<z.ZodString>;
|
|
15
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
16
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
17
|
+
};
|
|
18
|
+
outputSchema: {
|
|
19
|
+
type: z.ZodLiteral<"search_results">;
|
|
20
|
+
query: z.ZodString;
|
|
21
|
+
workspaceId: z.ZodNumber;
|
|
22
|
+
results: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
23
|
+
type: z.ZodLiteral<"thread">;
|
|
24
|
+
threadId: z.ZodString;
|
|
25
|
+
commentId: z.ZodOptional<z.ZodString>;
|
|
26
|
+
channelId: z.ZodOptional<z.ZodString>;
|
|
27
|
+
channelName: z.ZodOptional<z.ZodString>;
|
|
28
|
+
id: z.ZodString;
|
|
29
|
+
content: z.ZodString;
|
|
30
|
+
creatorId: z.ZodNumber;
|
|
31
|
+
creatorName: z.ZodOptional<z.ZodString>;
|
|
32
|
+
created: z.ZodString;
|
|
33
|
+
workspaceId: z.ZodNumber;
|
|
34
|
+
url: z.ZodString;
|
|
35
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
36
|
+
type: z.ZodLiteral<"conversation">;
|
|
37
|
+
conversationId: z.ZodString;
|
|
38
|
+
messageId: z.ZodString;
|
|
39
|
+
id: z.ZodString;
|
|
40
|
+
content: z.ZodString;
|
|
41
|
+
creatorId: z.ZodNumber;
|
|
42
|
+
creatorName: z.ZodOptional<z.ZodString>;
|
|
43
|
+
created: z.ZodString;
|
|
44
|
+
workspaceId: z.ZodNumber;
|
|
45
|
+
url: z.ZodString;
|
|
46
|
+
}, z.core.$strip>], "type">>;
|
|
47
|
+
totalResults: z.ZodNumber;
|
|
48
|
+
hasMore: z.ZodBoolean;
|
|
49
|
+
cursor: z.ZodOptional<z.ZodString>;
|
|
50
|
+
};
|
|
51
|
+
annotations: {
|
|
52
|
+
readOnlyHint: true;
|
|
53
|
+
destructiveHint: false;
|
|
54
|
+
idempotentHint: true;
|
|
55
|
+
};
|
|
56
|
+
execute(args: {
|
|
57
|
+
query: string;
|
|
58
|
+
workspaceId: number;
|
|
59
|
+
limit: number;
|
|
60
|
+
channelIds?: string[] | undefined;
|
|
61
|
+
authorIds?: number[] | undefined;
|
|
62
|
+
mentionSelf?: boolean | undefined;
|
|
63
|
+
dateFrom?: string | undefined;
|
|
64
|
+
dateTo?: string | undefined;
|
|
65
|
+
cursor?: string | undefined;
|
|
66
|
+
}, client: import("@doist/comms-sdk").CommsApi): Promise<{
|
|
67
|
+
textContent: string;
|
|
68
|
+
structuredContent: {
|
|
69
|
+
type: "search_results";
|
|
70
|
+
query: string;
|
|
71
|
+
workspaceId: number;
|
|
72
|
+
results: ({
|
|
73
|
+
type: "thread";
|
|
74
|
+
threadId: string;
|
|
75
|
+
id: string;
|
|
76
|
+
content: string;
|
|
77
|
+
creatorId: number;
|
|
78
|
+
created: string;
|
|
79
|
+
workspaceId: number;
|
|
80
|
+
url: string;
|
|
81
|
+
commentId?: string | undefined;
|
|
82
|
+
channelId?: string | undefined;
|
|
83
|
+
channelName?: string | undefined;
|
|
84
|
+
creatorName?: string | undefined;
|
|
85
|
+
} | {
|
|
86
|
+
type: "conversation";
|
|
87
|
+
conversationId: string;
|
|
88
|
+
messageId: string;
|
|
89
|
+
id: string;
|
|
90
|
+
content: string;
|
|
91
|
+
creatorId: number;
|
|
92
|
+
created: string;
|
|
93
|
+
workspaceId: number;
|
|
94
|
+
url: string;
|
|
95
|
+
creatorName?: string | undefined;
|
|
96
|
+
})[];
|
|
97
|
+
totalResults: number;
|
|
98
|
+
hasMore: boolean;
|
|
99
|
+
cursor?: string | undefined;
|
|
100
|
+
};
|
|
101
|
+
}>;
|
|
102
|
+
};
|
|
103
|
+
export { searchContent, type SearchContentStructured };
|