@todoist/comms-mcp 0.0.0-stage → 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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +208 -2
  3. package/dist/comms-tool.d.ts +14 -0
  4. package/dist/comms-tool.js +1 -0
  5. package/dist/index.d.ts +1324 -0
  6. package/dist/index.js +55 -0
  7. package/dist/main.d.ts +2 -0
  8. package/dist/main.js +24 -0
  9. package/dist/mcp-helpers.d.ts +17 -0
  10. package/dist/mcp-helpers.js +40 -0
  11. package/dist/mcp-server.d.ts +10 -0
  12. package/dist/mcp-server.js +78 -0
  13. package/dist/tool-registry.d.ts +3 -0
  14. package/dist/tool-registry.js +49 -0
  15. package/dist/tools/build-link.d.ts +63 -0
  16. package/dist/tools/build-link.js +93 -0
  17. package/dist/tools/channel-output.d.ts +19 -0
  18. package/dist/tools/channel-output.js +47 -0
  19. package/dist/tools/create-channel.d.ts +55 -0
  20. package/dist/tools/create-channel.js +44 -0
  21. package/dist/tools/create-conversation.d.ts +49 -0
  22. package/dist/tools/create-conversation.js +75 -0
  23. package/dist/tools/create-thread.d.ts +67 -0
  24. package/dist/tools/create-thread.js +126 -0
  25. package/dist/tools/delete-object.d.ts +44 -0
  26. package/dist/tools/delete-object.js +84 -0
  27. package/dist/tools/fetch-inbox.d.ts +91 -0
  28. package/dist/tools/fetch-inbox.js +227 -0
  29. package/dist/tools/get-groups.d.ts +62 -0
  30. package/dist/tools/get-groups.js +116 -0
  31. package/dist/tools/get-mentions.d.ts +97 -0
  32. package/dist/tools/get-mentions.js +126 -0
  33. package/dist/tools/get-users.d.ts +58 -0
  34. package/dist/tools/get-users.js +107 -0
  35. package/dist/tools/get-workspaces.d.ts +61 -0
  36. package/dist/tools/get-workspaces.js +120 -0
  37. package/dist/tools/list-channels.d.ts +47 -0
  38. package/dist/tools/list-channels.js +89 -0
  39. package/dist/tools/list-conversations.d.ts +66 -0
  40. package/dist/tools/list-conversations.js +212 -0
  41. package/dist/tools/load-conversation.d.ts +92 -0
  42. package/dist/tools/load-conversation.js +128 -0
  43. package/dist/tools/load-thread.d.ts +127 -0
  44. package/dist/tools/load-thread.js +175 -0
  45. package/dist/tools/mark-done.d.ts +111 -0
  46. package/dist/tools/mark-done.js +275 -0
  47. package/dist/tools/mark-read.d.ts +73 -0
  48. package/dist/tools/mark-read.js +214 -0
  49. package/dist/tools/react.d.ts +59 -0
  50. package/dist/tools/react.js +97 -0
  51. package/dist/tools/reply.d.ts +69 -0
  52. package/dist/tools/reply.js +122 -0
  53. package/dist/tools/search-content.d.ts +103 -0
  54. package/dist/tools/search-content.js +138 -0
  55. package/dist/tools/update-channel.d.ts +53 -0
  56. package/dist/tools/update-channel.js +45 -0
  57. package/dist/tools/update-object.d.ts +52 -0
  58. package/dist/tools/update-object.js +161 -0
  59. package/dist/tools/update-user-info.d.ts +55 -0
  60. package/dist/tools/update-user-info.js +270 -0
  61. package/dist/tools/user-info.d.ts +32 -0
  62. package/dist/tools/user-info.js +16 -0
  63. package/dist/tools/user-output.d.ts +9 -0
  64. package/dist/tools/user-output.js +28 -0
  65. package/dist/usage-tracking.d.ts +23 -0
  66. package/dist/usage-tracking.js +41 -0
  67. package/dist/utils/attachments.d.ts +24 -0
  68. package/dist/utils/attachments.js +57 -0
  69. package/dist/utils/concurrency.d.ts +20 -0
  70. package/dist/utils/concurrency.js +24 -0
  71. package/dist/utils/constants.d.ts +5 -0
  72. package/dist/utils/constants.js +5 -0
  73. package/dist/utils/degrade.d.ts +81 -0
  74. package/dist/utils/degrade.js +149 -0
  75. package/dist/utils/output-schemas.d.ts +1283 -0
  76. package/dist/utils/output-schemas.js +657 -0
  77. package/dist/utils/read-position.d.ts +21 -0
  78. package/dist/utils/read-position.js +37 -0
  79. package/dist/utils/search-results.d.ts +36 -0
  80. package/dist/utils/search-results.js +82 -0
  81. package/dist/utils/target-types.d.ts +114 -0
  82. package/dist/utils/target-types.js +58 -0
  83. package/dist/utils/test-helpers.d.ts +95 -0
  84. package/dist/utils/test-helpers.js +231 -0
  85. package/dist/utils/tool-names.d.ts +25 -0
  86. package/dist/utils/tool-names.js +25 -0
  87. package/dist/utils/url-helpers.d.ts +3 -0
  88. package/dist/utils/url-helpers.js +10 -0
  89. package/dist/utils/user-status.d.ts +8 -0
  90. package/dist/utils/user-status.js +33 -0
  91. package/dist/utils/validate-comms-token.d.ts +13 -0
  92. package/dist/utils/validate-comms-token.js +22 -0
  93. package/package.json +86 -5
@@ -0,0 +1,227 @@
1
+ import { ARCHIVE_FILTER_VALUES, getFullCommsURL } from '@doist/comms-sdk';
2
+ import { z } from 'zod';
3
+ import { limitedAll } from '../utils/concurrency.js';
4
+ import { degradeAllWithLog, resolveUsersWithLog } from '../utils/degrade.js';
5
+ import { FetchInboxOutputSchema } from '../utils/output-schemas.js';
6
+ import { ToolNames } from '../utils/tool-names.js';
7
+ const ArgsSchema = {
8
+ workspaceId: z.number().describe('The workspace ID to fetch inbox for.'),
9
+ sinceDate: z
10
+ .string()
11
+ .optional()
12
+ .describe('Optional date to get items since (YYYY-MM-DD format).'),
13
+ untilDate: z
14
+ .string()
15
+ .optional()
16
+ .describe('Optional date to get items until (YYYY-MM-DD format).'),
17
+ limit: z
18
+ .number()
19
+ .int()
20
+ .min(1)
21
+ .max(100)
22
+ .optional()
23
+ .default(50)
24
+ .describe('Maximum number of items to return.'),
25
+ onlyUnread: z.boolean().optional().default(false).describe('Only return unread items.'),
26
+ archiveFilter: z
27
+ .enum(ARCHIVE_FILTER_VALUES)
28
+ .optional()
29
+ .default('active')
30
+ .describe('Filter inbox threads by archive status. ' +
31
+ '"active" (default) shows only current threads, ' +
32
+ '"archived" shows only done/archived threads, ' +
33
+ '"all" shows both active and done threads.'),
34
+ };
35
+ // Names the conversation by its participants, falling back to the ID when none
36
+ // of them could be resolved.
37
+ function describeConversation(participantNames, conversationId) {
38
+ return participantNames.length > 0
39
+ ? `DM with ${participantNames.join(', ')}`
40
+ : `Conversation ${conversationId}`;
41
+ }
42
+ /**
43
+ * Helper function to load conversation details with participant information
44
+ */
45
+ async function loadConversationDetails(client, conversationIds) {
46
+ if (conversationIds.length === 0) {
47
+ return [];
48
+ }
49
+ // Load all conversation metadata in parallel
50
+ const conversations = await Promise.all(conversationIds.map((id) => client.conversations.getConversation(id)));
51
+ // Collect all unique user IDs from all conversations
52
+ const allUserIds = new Set();
53
+ for (const conv of conversations) {
54
+ for (const userId of conv.userIds) {
55
+ allUserIds.add(userId);
56
+ }
57
+ }
58
+ const workspaceId = conversations[0]?.workspaceId;
59
+ if (!workspaceId) {
60
+ return conversations.map((conversation) => ({ conversation, participants: [] }));
61
+ }
62
+ // A participant that cannot be resolved is dropped rather than failing the
63
+ // whole inbox: one unreachable user should not cost the caller every thread.
64
+ // Logged so a dropped name stays distinguishable from a backend outage.
65
+ const users = await resolveUsersWithLog(client, ToolNames.FETCH_INBOX, 'failed to resolve conversation participant', workspaceId, Array.from(allUserIds));
66
+ // Map conversations to include their participants
67
+ return conversations.map((conversation) => ({
68
+ conversation,
69
+ participants: conversation.userIds
70
+ .map((id) => users.get(id))
71
+ .filter((user) => !!user),
72
+ }));
73
+ }
74
+ const fetchInbox = {
75
+ name: ToolNames.FETCH_INBOX,
76
+ description: 'Fetch inbox view with threads, conversations, unread counts, and unread IDs. Provides a complete picture of the inbox state. Use archiveFilter "all" to include threads marked as done alongside active threads.',
77
+ parameters: ArgsSchema,
78
+ outputSchema: FetchInboxOutputSchema.shape,
79
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
80
+ async execute(args, client) {
81
+ const { workspaceId, sinceDate, untilDate, limit, onlyUnread } = args;
82
+ const archiveFilter = args.archiveFilter ?? 'active';
83
+ // Call all 4 endpoints in parallel for complete inbox picture
84
+ const [inboxThreads, unreadCount, unreadThreadsData, unreadConversationsData] = await Promise.all([
85
+ client.inbox.getInbox({
86
+ workspaceId,
87
+ newerThan: sinceDate ? new Date(sinceDate) : undefined,
88
+ olderThan: untilDate ? new Date(untilDate) : undefined,
89
+ limit,
90
+ archiveFilter,
91
+ }),
92
+ client.inbox.getCount(workspaceId),
93
+ client.threads.getUnread(workspaceId),
94
+ client.conversations.getUnread(workspaceId),
95
+ ]);
96
+ // Filter by unread if requested
97
+ let threads = inboxThreads.map((thread) => ({
98
+ ...thread,
99
+ isUnread: unreadThreadsData.data.some((ut) => ut.threadId === thread.id),
100
+ isArchived: thread.isArchived,
101
+ }));
102
+ const unreadThreads = threads.filter((t) => t.isUnread);
103
+ const unreadThreadsOriginal = inboxThreads.filter((thread) => unreadThreadsData.data.some((ut) => ut.threadId === thread.id));
104
+ if (onlyUnread) {
105
+ threads = unreadThreads;
106
+ }
107
+ // Load unread conversations if any exist
108
+ const unreadConversationsOriginal = unreadConversationsData.data;
109
+ const unreadConversationIds = unreadConversationsOriginal.map((uc) => uc.conversationId);
110
+ let conversationsWithDetails = [];
111
+ // Only load and display conversations if there are unread ones
112
+ if (unreadConversationIds.length > 0) {
113
+ const conversationDetails = await loadConversationDetails(client, unreadConversationIds);
114
+ conversationsWithDetails = conversationDetails.map((detail) => ({
115
+ ...detail,
116
+ isUnread: true,
117
+ }));
118
+ }
119
+ // Build text content
120
+ const lines = [
121
+ `# Inbox for Workspace ${workspaceId}`,
122
+ '',
123
+ `**Total Threads:** ${threads.length}`,
124
+ `**Unread Threads:** ${unreadThreads.length}`,
125
+ ];
126
+ // Only add conversation counts if there are unread conversations
127
+ if (conversationsWithDetails.length > 0) {
128
+ lines.push(`**Total Conversations:** ${conversationsWithDetails.length}`);
129
+ lines.push(`**Unread Conversations:** ${conversationsWithDetails.length}`);
130
+ }
131
+ lines.push('');
132
+ lines.push(`## Threads (${threads.length})`);
133
+ lines.push('');
134
+ // Dedupe channel IDs so we hit `channels.getChannel` once per channel
135
+ // even when the inbox page is dominated by threads from the same channel.
136
+ // `limitedAll` caps the burst on a large inbox page.
137
+ const uniqueChannelIds = Array.from(new Set(threads.map((t) => t.channelId)));
138
+ const channelResponses = await degradeAllWithLog(ToolNames.FETCH_INBOX, 'failed to resolve channel', uniqueChannelIds, (channelId) => client.channels.getChannel(channelId), { runner: limitedAll });
139
+ const channelInfo = channelResponses.reduce((acc, channel) => {
140
+ if (channel)
141
+ acc[channel.id] = channel;
142
+ return acc;
143
+ }, {});
144
+ if (threads.length === 0) {
145
+ lines.push('_No threads in inbox_');
146
+ lines.push('');
147
+ }
148
+ else {
149
+ for (const thread of threads) {
150
+ const channel = channelInfo[thread.channelId];
151
+ const channelDetails = !channel ? `(Channel ${thread.channelId})` : '';
152
+ if (channel) {
153
+ thread.title = `[${channel.name}] ${thread.title}`;
154
+ }
155
+ const archivedBadge = thread.isArchived ? ' [archived]' : '';
156
+ const unreadBadge = thread.isUnread ? ' [unread]' : '';
157
+ const starBadge = thread.isSaved ? ' [saved]' : '';
158
+ lines.push(`- ${thread.title}${archivedBadge}${unreadBadge}${starBadge}${channelDetails} (ID: ${thread.id})`);
159
+ }
160
+ lines.push('');
161
+ }
162
+ // Add conversations section only if there are unread conversations
163
+ if (conversationsWithDetails.length > 0) {
164
+ lines.push(`## Conversations (${conversationsWithDetails.length})`);
165
+ lines.push('');
166
+ for (const convDetail of conversationsWithDetails) {
167
+ const { conversation, participants } = convDetail;
168
+ // Build a human-readable title from participant names
169
+ const participantNames = participants.map((p) => p.fullName);
170
+ const conversationTitle = conversation.title || describeConversation(participantNames, conversation.id);
171
+ const unreadBadge = convDetail.isUnread ? ' [unread]' : '';
172
+ lines.push(`- ${conversationTitle}${unreadBadge} (ID: ${conversation.id})`);
173
+ }
174
+ lines.push('');
175
+ }
176
+ if (unreadCount > 0 || conversationsWithDetails.length > 0) {
177
+ lines.push('## Next Steps');
178
+ lines.push('');
179
+ lines.push('- Use `load-thread` to read specific threads with their comments');
180
+ lines.push('- Use `load-conversation` to read specific conversations with their messages');
181
+ lines.push('- Use `mark-done` to mark items as read and archive them');
182
+ }
183
+ const structuredContent = {
184
+ type: 'inbox_data',
185
+ workspaceId,
186
+ threads: threads.map((t) => ({
187
+ id: t.id,
188
+ title: t.title,
189
+ channelId: t.channelId,
190
+ channelName: channelInfo[t.channelId]?.name,
191
+ creator: t.creator,
192
+ isUnread: t.isUnread,
193
+ isArchived: t.isArchived,
194
+ isStarred: Boolean(t.isSaved),
195
+ threadUrl: t.url ??
196
+ getFullCommsURL({ workspaceId, channelId: t.channelId, threadId: t.id }),
197
+ })),
198
+ conversations: conversationsWithDetails.map((cd) => {
199
+ const { conversation, participants } = cd;
200
+ const participantNames = participants.map((p) => p.fullName);
201
+ return {
202
+ id: conversation.id,
203
+ title: conversation.title ||
204
+ describeConversation(participantNames, conversation.id),
205
+ userIds: conversation.userIds,
206
+ participantNames,
207
+ isUnread: cd.isUnread,
208
+ conversationUrl: conversation.url ??
209
+ getFullCommsURL({
210
+ workspaceId: conversation.workspaceId,
211
+ conversationId: conversation.id,
212
+ }),
213
+ };
214
+ }),
215
+ unreadCount: unreadThreads.length,
216
+ unreadThreads: unreadThreadsOriginal,
217
+ unreadConversations: unreadConversationsOriginal,
218
+ totalThreads: threads.length,
219
+ totalConversations: conversationsWithDetails.length,
220
+ };
221
+ return {
222
+ textContent: lines.join('\n'),
223
+ structuredContent,
224
+ };
225
+ },
226
+ };
227
+ export { fetchInbox };
@@ -0,0 +1,62 @@
1
+ import type { CommsApi } from '@doist/comms-sdk';
2
+ import { z } from 'zod';
3
+ import { type GetGroupsOutput } from '../utils/output-schemas.js';
4
+ type GetGroupsStructured = GetGroupsOutput;
5
+ declare const getGroups: {
6
+ name: "get-groups";
7
+ description: string;
8
+ parameters: {
9
+ workspaceId: z.ZodNumber;
10
+ groupIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
11
+ searchText: z.ZodOptional<z.ZodString>;
12
+ includeMembers: z.ZodOptional<z.ZodBoolean>;
13
+ };
14
+ outputSchema: {
15
+ type: z.ZodLiteral<"get_groups">;
16
+ workspaceId: z.ZodNumber;
17
+ groups: z.ZodArray<z.ZodObject<{
18
+ id: z.ZodString;
19
+ name: z.ZodString;
20
+ workspaceId: z.ZodNumber;
21
+ memberCount: z.ZodNumber;
22
+ members: z.ZodOptional<z.ZodArray<z.ZodObject<{
23
+ id: z.ZodNumber;
24
+ name: z.ZodOptional<z.ZodString>;
25
+ email: z.ZodOptional<z.ZodString>;
26
+ }, z.core.$strip>>>;
27
+ }, z.core.$strip>>;
28
+ totalGroups: z.ZodNumber;
29
+ filteredGroups: z.ZodNumber;
30
+ };
31
+ annotations: {
32
+ readOnlyHint: true;
33
+ destructiveHint: false;
34
+ idempotentHint: true;
35
+ };
36
+ execute(args: {
37
+ workspaceId: number;
38
+ groupIds?: string[] | undefined;
39
+ searchText?: string | undefined;
40
+ includeMembers?: boolean | undefined;
41
+ }, client: CommsApi): Promise<{
42
+ textContent: string;
43
+ structuredContent: {
44
+ type: "get_groups";
45
+ workspaceId: number;
46
+ groups: {
47
+ id: string;
48
+ name: string;
49
+ workspaceId: number;
50
+ memberCount: number;
51
+ members?: {
52
+ id: number;
53
+ name?: string | undefined;
54
+ email?: string | undefined;
55
+ }[] | undefined;
56
+ }[];
57
+ totalGroups: number;
58
+ filteredGroups: number;
59
+ };
60
+ }>;
61
+ };
62
+ export { getGroups, type GetGroupsStructured };
@@ -0,0 +1,116 @@
1
+ import { z } from 'zod';
2
+ import { degradeAllWithLog, degradeWithLog } from '../utils/degrade.js';
3
+ import { GetGroupsOutputSchema, } from '../utils/output-schemas.js';
4
+ import { ToolNames } from '../utils/tool-names.js';
5
+ const ArgsSchema = {
6
+ workspaceId: z.number().describe('The workspace ID to get groups from.'),
7
+ groupIds: z
8
+ .array(z.string())
9
+ .optional()
10
+ .describe('Optional array of specific group IDs to fetch. If not provided or empty array, fetches all workspace groups.'),
11
+ searchText: z
12
+ .string()
13
+ .optional()
14
+ .describe('Optional search text to filter groups by name (case-insensitive).'),
15
+ includeMembers: z
16
+ .boolean()
17
+ .optional()
18
+ .describe('When true, list each group\'s members (user ID, name, and email) instead of just a member count. Defaults to false. Use it to answer questions like "who is in the Automations squad?" or to expand a group into individual recipients.'),
19
+ };
20
+ /**
21
+ * Resolves member user IDs to names/emails with a single workspace-users fetch,
22
+ * so member lists cost one request regardless of how many groups are returned.
23
+ *
24
+ * Returns null when the directory is unreadable (e.g. a token without user
25
+ * scope). Callers degrade to bare user IDs rather than failing the whole
26
+ * listing, but say so in the output — unlike list-conversations, where names
27
+ * are decoration, here the member list is what was asked for, so a silent
28
+ * drop to IDs would misrepresent the answer.
29
+ */
30
+ async function fetchMemberLookup(client, workspaceId) {
31
+ const users = await client.workspaceUsers
32
+ .getWorkspaceUsers({ workspaceId })
33
+ .catch(degradeWithLog(ToolNames.GET_GROUPS, 'failed to load workspace roster', { workspaceId }, null));
34
+ if (!users) {
35
+ return null;
36
+ }
37
+ return new Map(users.map((user) => [
38
+ user.id,
39
+ { id: user.id, name: user.fullName, ...(user.email && { email: user.email }) },
40
+ ]));
41
+ }
42
+ function toMembers(userIds, lookup) {
43
+ return userIds.map((id) => lookup?.get(id) ?? { id });
44
+ }
45
+ const getGroups = {
46
+ name: ToolNames.GET_GROUPS,
47
+ description: 'Get groups from a workspace. Retrieves all workspace groups by default, or specific groups if groupIds array is provided. Supports optional case-insensitive search filtering by group name. Set includeMembers to true to also list who belongs to each group. Use this before passing group IDs to tools that support group notifications.',
48
+ parameters: ArgsSchema,
49
+ outputSchema: GetGroupsOutputSchema.shape,
50
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
51
+ async execute(args, client) {
52
+ const { workspaceId, groupIds, searchText, includeMembers } = args;
53
+ const requestedGroupIds = groupIds && groupIds.length > 0 ? [...new Set(groupIds)] : undefined;
54
+ const groups = requestedGroupIds
55
+ ? (await degradeAllWithLog(ToolNames.GET_GROUPS, 'failed to resolve group', requestedGroupIds, (id) => client.groups.getGroup({ id, workspaceId })))
56
+ .filter((g) => g !== null)
57
+ .filter((group) => group.workspaceId === workspaceId)
58
+ : await client.groups.getGroups(workspaceId);
59
+ const totalGroups = groups.length;
60
+ let filteredGroups = groups;
61
+ if (searchText) {
62
+ const searchLower = searchText.toLowerCase();
63
+ filteredGroups = groups.filter((group) => group.name.toLowerCase().includes(searchLower));
64
+ }
65
+ // Nothing to resolve if every matched group is empty, so skip the directory read.
66
+ const hasMembersToResolve = filteredGroups.some((group) => group.userIds.length > 0);
67
+ const memberLookup = includeMembers && hasMembersToResolve
68
+ ? await fetchMemberLookup(client, workspaceId)
69
+ : undefined;
70
+ const membersUnresolved = memberLookup === null;
71
+ const lines = ['# Workspace Groups', ''];
72
+ lines.push(`**Workspace ID:** ${workspaceId}`);
73
+ lines.push(`**Total Groups:** ${totalGroups}${searchText ? ` (${filteredGroups.length} matching search)` : ''}`);
74
+ if (membersUnresolved) {
75
+ lines.push('**Note:** Member names could not be looked up (the workspace directory was unavailable), so members are listed by user ID only.');
76
+ }
77
+ lines.push('');
78
+ if (filteredGroups.length === 0) {
79
+ lines.push('No groups found.');
80
+ }
81
+ else {
82
+ for (const group of filteredGroups) {
83
+ lines.push(`## ${group.name}`);
84
+ lines.push(`**ID:** ${group.id}`);
85
+ lines.push(`**Members:** ${group.userIds.length}`);
86
+ if (includeMembers) {
87
+ for (const member of toMembers(group.userIds, memberLookup)) {
88
+ const name = member.name ?? `user:${member.id}`;
89
+ const email = member.email ? ` <${member.email}>` : '';
90
+ lines.push(`- ${name} (id:${member.id})${email}`);
91
+ }
92
+ }
93
+ lines.push('');
94
+ }
95
+ }
96
+ const textContent = lines.join('\n');
97
+ const structuredContent = {
98
+ type: 'get_groups',
99
+ workspaceId,
100
+ groups: filteredGroups.map((group) => ({
101
+ id: group.id,
102
+ name: group.name,
103
+ workspaceId: group.workspaceId,
104
+ memberCount: group.userIds.length,
105
+ ...(includeMembers && { members: toMembers(group.userIds, memberLookup) }),
106
+ })),
107
+ totalGroups,
108
+ filteredGroups: filteredGroups.length,
109
+ };
110
+ return {
111
+ textContent,
112
+ structuredContent,
113
+ };
114
+ },
115
+ };
116
+ export { getGroups };
@@ -0,0 +1,97 @@
1
+ import { z } from 'zod';
2
+ import { type GetMentionsOutput } from '../utils/output-schemas.js';
3
+ type GetMentionsStructured = GetMentionsOutput;
4
+ declare const getMentions: {
5
+ name: "get-mentions";
6
+ description: string;
7
+ parameters: {
8
+ workspaceId: z.ZodNumber;
9
+ channelIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
10
+ authorIds: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
11
+ dateFrom: z.ZodOptional<z.ZodString>;
12
+ dateTo: z.ZodOptional<z.ZodString>;
13
+ limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
14
+ cursor: z.ZodOptional<z.ZodString>;
15
+ };
16
+ outputSchema: {
17
+ type: z.ZodLiteral<"mentions_results">;
18
+ workspaceId: z.ZodNumber;
19
+ results: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
20
+ type: z.ZodLiteral<"thread">;
21
+ threadId: z.ZodString;
22
+ commentId: z.ZodOptional<z.ZodString>;
23
+ channelId: z.ZodOptional<z.ZodString>;
24
+ channelName: z.ZodOptional<z.ZodString>;
25
+ id: z.ZodString;
26
+ content: z.ZodString;
27
+ creatorId: z.ZodNumber;
28
+ creatorName: z.ZodOptional<z.ZodString>;
29
+ created: z.ZodString;
30
+ workspaceId: z.ZodNumber;
31
+ url: z.ZodString;
32
+ }, z.core.$strip>, z.ZodObject<{
33
+ type: z.ZodLiteral<"conversation">;
34
+ conversationId: z.ZodString;
35
+ messageId: z.ZodString;
36
+ id: z.ZodString;
37
+ content: z.ZodString;
38
+ creatorId: z.ZodNumber;
39
+ creatorName: z.ZodOptional<z.ZodString>;
40
+ created: z.ZodString;
41
+ workspaceId: z.ZodNumber;
42
+ url: z.ZodString;
43
+ }, z.core.$strip>], "type">>;
44
+ totalResults: z.ZodNumber;
45
+ hasMore: z.ZodBoolean;
46
+ cursor: z.ZodOptional<z.ZodString>;
47
+ };
48
+ annotations: {
49
+ readOnlyHint: true;
50
+ destructiveHint: false;
51
+ idempotentHint: true;
52
+ };
53
+ execute(args: {
54
+ workspaceId: number;
55
+ limit: number;
56
+ channelIds?: string[] | undefined;
57
+ authorIds?: number[] | undefined;
58
+ dateFrom?: string | undefined;
59
+ dateTo?: string | undefined;
60
+ cursor?: string | undefined;
61
+ }, client: import("@doist/comms-sdk").CommsApi): Promise<{
62
+ textContent: string;
63
+ structuredContent: {
64
+ type: "mentions_results";
65
+ workspaceId: number;
66
+ results: ({
67
+ type: "thread";
68
+ threadId: string;
69
+ id: string;
70
+ content: string;
71
+ creatorId: number;
72
+ created: string;
73
+ workspaceId: number;
74
+ url: string;
75
+ commentId?: string | undefined;
76
+ channelId?: string | undefined;
77
+ channelName?: string | undefined;
78
+ creatorName?: string | undefined;
79
+ } | {
80
+ type: "conversation";
81
+ conversationId: string;
82
+ messageId: string;
83
+ id: string;
84
+ content: string;
85
+ creatorId: number;
86
+ created: string;
87
+ workspaceId: number;
88
+ url: string;
89
+ creatorName?: string | undefined;
90
+ })[];
91
+ totalResults: number;
92
+ hasMore: boolean;
93
+ cursor?: string | undefined;
94
+ };
95
+ }>;
96
+ };
97
+ export { getMentions, type GetMentionsStructured };
@@ -0,0 +1,126 @@
1
+ import { z } from 'zod';
2
+ import { degradeAllWithLog, resolveUsersWithLog } from '../utils/degrade.js';
3
+ import { GetMentionsOutputSchema } from '../utils/output-schemas.js';
4
+ import { toRawSearchResults, toSearchResultItems } from '../utils/search-results.js';
5
+ import { ToolNames } from '../utils/tool-names.js';
6
+ const ArgsSchema = {
7
+ workspaceId: z.number().describe('The workspace ID to search in.'),
8
+ channelIds: z.array(z.string()).optional().describe('Filter by channel IDs.'),
9
+ authorIds: z.array(z.number()).optional().describe('Filter by author user IDs.'),
10
+ dateFrom: z.string().optional().describe('Start date for filtering (YYYY-MM-DD).'),
11
+ dateTo: z.string().optional().describe('End date for filtering (YYYY-MM-DD).'),
12
+ limit: z
13
+ .number()
14
+ .int()
15
+ .min(1)
16
+ .max(100)
17
+ .optional()
18
+ .default(50)
19
+ .describe('Maximum number of results to return.'),
20
+ cursor: z.string().optional().describe('Cursor for pagination.'),
21
+ };
22
+ const getMentions = {
23
+ name: ToolNames.GET_MENTIONS,
24
+ description: 'Fetch threads, comments, and messages that mention the current user. Supports filtering by channel, author, and date range. Use this instead of search-content when no keyword query is needed.',
25
+ parameters: ArgsSchema,
26
+ outputSchema: GetMentionsOutputSchema.shape,
27
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
28
+ async execute(args, client) {
29
+ const { workspaceId, channelIds, authorIds, dateFrom, dateTo, limit, cursor } = args;
30
+ const response = await client.search.search({
31
+ workspaceId,
32
+ mentionSelf: true,
33
+ channelIds,
34
+ authorIds,
35
+ dateFrom,
36
+ dateTo,
37
+ limit,
38
+ cursor,
39
+ });
40
+ const results = toRawSearchResults(response.items, 'get-mentions');
41
+ const hasMore = response.hasMore;
42
+ const responseCursor = response.nextCursorMark;
43
+ const userLookup = {};
44
+ let channelLookup = {};
45
+ if (results.length > 0) {
46
+ const userIds = new Set();
47
+ const channelIdSet = new Set();
48
+ for (const result of results) {
49
+ userIds.add(result.creatorId);
50
+ if (result.type === 'thread' && result.channelId) {
51
+ channelIdSet.add(result.channelId);
52
+ }
53
+ }
54
+ const uniqueUserIds = Array.from(userIds);
55
+ const uniqueChannelIds = Array.from(channelIdSet);
56
+ const [users, channels] = await Promise.all([
57
+ resolveUsersWithLog(client, ToolNames.GET_MENTIONS, 'failed to resolve mention author', workspaceId, uniqueUserIds),
58
+ degradeAllWithLog(ToolNames.GET_MENTIONS, 'failed to resolve channel', uniqueChannelIds, (id) => client.channels.getChannel(id)),
59
+ ]);
60
+ for (const [id, user] of users) {
61
+ userLookup[id] = user.fullName;
62
+ }
63
+ channelLookup = channels.reduce((acc, channel) => {
64
+ if (channel)
65
+ acc[channel.id] = channel.name;
66
+ return acc;
67
+ }, {});
68
+ }
69
+ const lines = [`# Mentions in Workspace ${workspaceId}`, ''];
70
+ lines.push(`**Results Found:** ${results.length}`);
71
+ lines.push(`**More Available:** ${hasMore ? 'Yes' : 'No'}`);
72
+ lines.push('');
73
+ if (results.length === 0) {
74
+ lines.push('_No mentions found_');
75
+ }
76
+ else {
77
+ lines.push('## Results');
78
+ lines.push('');
79
+ for (const result of results) {
80
+ const date = result.created.split('T')[0];
81
+ const typeLabel = result.type.charAt(0).toUpperCase() + result.type.slice(1);
82
+ const creatorName = userLookup[result.creatorId];
83
+ lines.push(`### ${typeLabel} ${result.id}`);
84
+ lines.push(`**Created:** ${date} | **Creator:** ${creatorName} (${result.creatorId})`);
85
+ if (result.type === 'thread') {
86
+ lines.push(`**Thread:** ${result.threadId}`);
87
+ if (result.commentId) {
88
+ lines.push(`**Comment:** ${result.commentId}`);
89
+ }
90
+ if (result.channelId) {
91
+ const channelName = channelLookup[result.channelId];
92
+ lines.push(`**Channel:** ${channelName} (${result.channelId})`);
93
+ }
94
+ }
95
+ else {
96
+ lines.push(`**Conversation:** ${result.conversationId}`);
97
+ lines.push(`**Message:** ${result.messageId}`);
98
+ }
99
+ lines.push('');
100
+ const contentPreview = result.content.length > 200
101
+ ? `${result.content.substring(0, 200)}...`
102
+ : result.content;
103
+ lines.push(contentPreview);
104
+ lines.push('');
105
+ }
106
+ }
107
+ if (hasMore) {
108
+ lines.push('## Next Steps');
109
+ lines.push('');
110
+ lines.push('More results available. Use the cursor to fetch the next page.');
111
+ }
112
+ const structuredContent = {
113
+ type: 'mentions_results',
114
+ workspaceId,
115
+ results: toSearchResultItems(results, { workspaceId, userLookup, channelLookup }),
116
+ totalResults: results.length,
117
+ hasMore,
118
+ cursor: responseCursor,
119
+ };
120
+ return {
121
+ textContent: lines.join('\n'),
122
+ structuredContent,
123
+ };
124
+ },
125
+ };
126
+ export { getMentions };