@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.
- package/LICENSE +21 -0
- package/README.md +208 -2
- 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 +86 -5
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { isNotFound } from '@doist/comms-sdk';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { limitedAll } from '../utils/concurrency.js';
|
|
4
|
+
import { resolveUsersWithLog } from '../utils/degrade.js';
|
|
5
|
+
import { ListConversationsOutputSchema } from '../utils/output-schemas.js';
|
|
6
|
+
import { ToolNames } from '../utils/tool-names.js';
|
|
7
|
+
import { getConversationUrl } from '../utils/url-helpers.js';
|
|
8
|
+
const ArgsSchema = {
|
|
9
|
+
workspaceId: z.number().describe('The workspace ID to list conversations from.'),
|
|
10
|
+
includeArchived: z
|
|
11
|
+
.boolean()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe('Whether to include archived conversations. If true, both active and archived conversations are returned. Defaults to false (active conversations only).'),
|
|
14
|
+
searchText: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('Optional search text to find group chats by title. Matches whole words in the titles of titled group chats only: direct messages, untitled group chats, and participant names do not match.'),
|
|
18
|
+
limit: z
|
|
19
|
+
.number()
|
|
20
|
+
.int()
|
|
21
|
+
.min(1)
|
|
22
|
+
.max(100)
|
|
23
|
+
.optional()
|
|
24
|
+
.describe('Maximum number of search results per page. Only used with searchText. Defaults to 20.'),
|
|
25
|
+
cursor: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe('Cursor for the next page of search results. Only used with searchText.'),
|
|
29
|
+
};
|
|
30
|
+
// Only resolve names for the first few participants of each conversation. A large
|
|
31
|
+
// group DM would otherwise produce an unbounded participant string; callers that
|
|
32
|
+
// need every participant should load the conversation directly. The full set of
|
|
33
|
+
// participant IDs is still returned in `userIds`.
|
|
34
|
+
const MAX_DISPLAYED_PARTICIPANTS = 5;
|
|
35
|
+
const DEFAULT_SEARCH_LIMIT = 20;
|
|
36
|
+
// Resolve user IDs to names. IDs that can't be resolved are simply absent from
|
|
37
|
+
// the returned map.
|
|
38
|
+
async function resolveParticipantNames(client, workspaceId, userIds) {
|
|
39
|
+
const users = await resolveUsersWithLog(client, ToolNames.LIST_CONVERSATIONS, 'failed to resolve conversation participant', workspaceId, userIds);
|
|
40
|
+
const lookup = {};
|
|
41
|
+
for (const [id, user] of users) {
|
|
42
|
+
lookup[id] = user.fullName;
|
|
43
|
+
}
|
|
44
|
+
return lookup;
|
|
45
|
+
}
|
|
46
|
+
async function searchConversations(client, workspaceId, includeArchived, { searchText, limit, cursor }) {
|
|
47
|
+
const { items, hasMore, nextCursorMark } = await client.search.search({
|
|
48
|
+
type: 'conversations',
|
|
49
|
+
query: searchText,
|
|
50
|
+
workspaceId,
|
|
51
|
+
limit,
|
|
52
|
+
cursor,
|
|
53
|
+
});
|
|
54
|
+
const conversationIds = new Set(items.flatMap((hit) => hit.type === 'conversation' && hit.conversationId ? [hit.conversationId] : []));
|
|
55
|
+
const conversations = await limitedAll([...conversationIds], async (id) => {
|
|
56
|
+
try {
|
|
57
|
+
return await client.conversations.getConversation(id);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (isNotFound(error)) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const dropped = conversations.filter((conversation) => conversation === null).length;
|
|
67
|
+
if (dropped > 0) {
|
|
68
|
+
console.error(`${ToolNames.LIST_CONVERSATIONS}: dropped search hits for missing conversations`, { dropped, of: conversations.length });
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
conversations: conversations.filter((conversation) => conversation !== null && (includeArchived || !conversation.archived)),
|
|
72
|
+
hasMore,
|
|
73
|
+
cursor: nextCursorMark,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function getSearchPageOutput(searchPage) {
|
|
77
|
+
if (!searchPage) {
|
|
78
|
+
return { lines: [], fields: {} };
|
|
79
|
+
}
|
|
80
|
+
const { hasMore, cursor } = searchPage;
|
|
81
|
+
return {
|
|
82
|
+
lines: hasMore
|
|
83
|
+
? [
|
|
84
|
+
'## Next Steps',
|
|
85
|
+
'',
|
|
86
|
+
'More results available. Use the cursor to fetch the next page.',
|
|
87
|
+
]
|
|
88
|
+
: [],
|
|
89
|
+
fields: { hasMore, ...(cursor && { cursor }) },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function generateConversationsList(client, workspaceId, includeArchived, search) {
|
|
93
|
+
// By default only fetch active conversations; optionally include archived ones too
|
|
94
|
+
let conversations;
|
|
95
|
+
let searchPage;
|
|
96
|
+
if (search) {
|
|
97
|
+
searchPage = await searchConversations(client, workspaceId, includeArchived, search);
|
|
98
|
+
conversations = searchPage.conversations;
|
|
99
|
+
}
|
|
100
|
+
else if (includeArchived) {
|
|
101
|
+
const [active, archived] = await Promise.all([
|
|
102
|
+
client.conversations.getConversations({ workspaceId }),
|
|
103
|
+
client.conversations.getConversations({ workspaceId, archived: true }),
|
|
104
|
+
]);
|
|
105
|
+
conversations = [...active, ...archived];
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
conversations = await client.conversations.getConversations({ workspaceId });
|
|
109
|
+
}
|
|
110
|
+
const searchPageOutput = getSearchPageOutput(searchPage);
|
|
111
|
+
if (conversations.length === 0) {
|
|
112
|
+
return {
|
|
113
|
+
textContent: [
|
|
114
|
+
'# Conversations',
|
|
115
|
+
'',
|
|
116
|
+
'No conversations found.',
|
|
117
|
+
...(searchPageOutput.lines.length > 0 ? ['', ...searchPageOutput.lines] : []),
|
|
118
|
+
].join('\n'),
|
|
119
|
+
structuredContent: {
|
|
120
|
+
type: 'list_conversations',
|
|
121
|
+
workspaceId,
|
|
122
|
+
conversations: [],
|
|
123
|
+
totalConversations: 0,
|
|
124
|
+
...searchPageOutput.fields,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// Only the first few participants of each conversation are displayed, so only
|
|
129
|
+
// those names need resolving. Collect them into a deduplicated set.
|
|
130
|
+
const displayUserIdsByConversation = new Map();
|
|
131
|
+
const idsToResolve = new Set();
|
|
132
|
+
for (const conversation of conversations) {
|
|
133
|
+
const displayIds = conversation.userIds.slice(0, MAX_DISPLAYED_PARTICIPANTS);
|
|
134
|
+
displayUserIdsByConversation.set(conversation.id, displayIds);
|
|
135
|
+
for (const userId of displayIds) {
|
|
136
|
+
idsToResolve.add(userId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const participantLookup = await resolveParticipantNames(client, workspaceId, Array.from(idsToResolve));
|
|
140
|
+
// Names for the displayed participants, positionally aligned with the first
|
|
141
|
+
// entries of the conversation's `userIds`. An ID that couldn't be resolved to a
|
|
142
|
+
// name falls back to its stringified ID so alignment is preserved.
|
|
143
|
+
function displayNamesFor(displayIds) {
|
|
144
|
+
return displayIds.map((id) => participantLookup[id] ?? String(id));
|
|
145
|
+
}
|
|
146
|
+
const lines = ['# Conversations', ''];
|
|
147
|
+
lines.push(`Found ${conversations.length} conversation${conversations.length === 1 ? '' : 's'} in workspace ${workspaceId}:`, '');
|
|
148
|
+
for (const conversation of conversations) {
|
|
149
|
+
const conversationUrl = conversation.url ?? getConversationUrl(workspaceId, conversation.id);
|
|
150
|
+
const heading = conversation.title?.trim()
|
|
151
|
+
? conversation.title
|
|
152
|
+
: `Conversation ${conversation.id}`;
|
|
153
|
+
lines.push(`## [${heading}](${conversationUrl})`);
|
|
154
|
+
lines.push(`**ID:** ${conversation.id}`);
|
|
155
|
+
lines.push(`**Archived:** ${conversation.archived ? 'Yes' : 'No'}`);
|
|
156
|
+
lines.push(`**Last Active:** ${conversation.lastActive.toISOString()}`);
|
|
157
|
+
const displayIds = displayUserIdsByConversation.get(conversation.id) ?? [];
|
|
158
|
+
const displayNames = displayNamesFor(displayIds);
|
|
159
|
+
const remaining = conversation.userIds.length - displayIds.length;
|
|
160
|
+
const participantsSummary = remaining > 0
|
|
161
|
+
? `${displayNames.join(', ')}, and ${remaining} more`
|
|
162
|
+
: displayNames.join(', ');
|
|
163
|
+
lines.push(`**Participants:** ${participantsSummary}`);
|
|
164
|
+
if (conversation.snippet) {
|
|
165
|
+
lines.push(`**Snippet:** ${conversation.snippet}`);
|
|
166
|
+
}
|
|
167
|
+
lines.push('');
|
|
168
|
+
}
|
|
169
|
+
lines.push(...searchPageOutput.lines);
|
|
170
|
+
const textContent = lines.join('\n');
|
|
171
|
+
const structuredContent = {
|
|
172
|
+
type: 'list_conversations',
|
|
173
|
+
workspaceId,
|
|
174
|
+
conversations: conversations.map((conversation) => {
|
|
175
|
+
const displayIds = displayUserIdsByConversation.get(conversation.id) ?? [];
|
|
176
|
+
const displayNames = displayNamesFor(displayIds);
|
|
177
|
+
return {
|
|
178
|
+
id: conversation.id,
|
|
179
|
+
workspaceId: conversation.workspaceId,
|
|
180
|
+
...(conversation.title && { title: conversation.title }),
|
|
181
|
+
userIds: conversation.userIds,
|
|
182
|
+
...(displayNames.length > 0 && { participantNames: displayNames }),
|
|
183
|
+
archived: conversation.archived,
|
|
184
|
+
lastActive: conversation.lastActive.toISOString(),
|
|
185
|
+
...(conversation.snippet && { snippet: conversation.snippet }),
|
|
186
|
+
conversationUrl: conversation.url ?? getConversationUrl(workspaceId, conversation.id),
|
|
187
|
+
};
|
|
188
|
+
}),
|
|
189
|
+
totalConversations: conversations.length,
|
|
190
|
+
...searchPageOutput.fields,
|
|
191
|
+
};
|
|
192
|
+
return { textContent, structuredContent };
|
|
193
|
+
}
|
|
194
|
+
const listConversations = {
|
|
195
|
+
name: ToolNames.LIST_CONVERSATIONS,
|
|
196
|
+
description: 'List conversations (direct messages and group chats) in a workspace, or find group chats by title with searchText. Without searchText, returns the first page of conversations. searchText matches whole words in the titles of titled group chats only: direct messages, untitled group chats, and participant names do not match. Its results are paged with limit and cursor, and a page can have fewer results than limit, so follow hasMore. By default returns only active conversations; set includeArchived to true to also include archived conversations. Returns conversation IDs, titles, the full list of participant user IDs (with names resolved for the first few), archive status, last-active timestamps, snippets, and URLs.',
|
|
197
|
+
parameters: ArgsSchema,
|
|
198
|
+
outputSchema: ListConversationsOutputSchema.shape,
|
|
199
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
200
|
+
async execute(args, client) {
|
|
201
|
+
const { workspaceId, includeArchived = false, searchText, limit, cursor } = args;
|
|
202
|
+
if (!searchText && (limit !== undefined || cursor !== undefined)) {
|
|
203
|
+
throw new Error('`limit` and `cursor` require `searchText`.');
|
|
204
|
+
}
|
|
205
|
+
const result = await generateConversationsList(client, workspaceId, includeArchived, searchText ? { searchText, limit: limit ?? DEFAULT_SEARCH_LIMIT, cursor } : undefined);
|
|
206
|
+
return {
|
|
207
|
+
textContent: result.textContent,
|
|
208
|
+
structuredContent: result.structuredContent,
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
export { listConversations };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type OutputAttachment } from '../utils/output-schemas.js';
|
|
3
|
+
type LoadConversationStructured = {
|
|
4
|
+
type: 'conversation_data';
|
|
5
|
+
conversation: {
|
|
6
|
+
id: string;
|
|
7
|
+
workspaceId: number;
|
|
8
|
+
userIds: number[];
|
|
9
|
+
archived: boolean;
|
|
10
|
+
lastActive: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
conversationUrl: string;
|
|
13
|
+
};
|
|
14
|
+
messages: Array<{
|
|
15
|
+
id: string;
|
|
16
|
+
content: string;
|
|
17
|
+
creatorId: number;
|
|
18
|
+
creatorName?: string;
|
|
19
|
+
conversationId: string;
|
|
20
|
+
posted: string;
|
|
21
|
+
messageUrl: string;
|
|
22
|
+
attachments?: OutputAttachment[];
|
|
23
|
+
}>;
|
|
24
|
+
totalMessages: number;
|
|
25
|
+
};
|
|
26
|
+
declare const loadConversation: {
|
|
27
|
+
name: "load-conversation";
|
|
28
|
+
description: string;
|
|
29
|
+
parameters: {
|
|
30
|
+
conversationId: z.ZodString;
|
|
31
|
+
newerThanDate: z.ZodOptional<z.ZodString>;
|
|
32
|
+
olderThanDate: z.ZodOptional<z.ZodString>;
|
|
33
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
34
|
+
includeParticipants: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
35
|
+
};
|
|
36
|
+
outputSchema: {
|
|
37
|
+
type: z.ZodLiteral<"conversation_data">;
|
|
38
|
+
conversation: z.ZodObject<{
|
|
39
|
+
id: z.ZodString;
|
|
40
|
+
workspaceId: z.ZodNumber;
|
|
41
|
+
userIds: z.ZodArray<z.ZodNumber>;
|
|
42
|
+
archived: z.ZodBoolean;
|
|
43
|
+
lastActive: z.ZodString;
|
|
44
|
+
title: z.ZodOptional<z.ZodString>;
|
|
45
|
+
conversationUrl: z.ZodString;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
messages: z.ZodArray<z.ZodObject<{
|
|
48
|
+
id: z.ZodString;
|
|
49
|
+
content: z.ZodString;
|
|
50
|
+
creatorId: z.ZodNumber;
|
|
51
|
+
creatorName: z.ZodOptional<z.ZodString>;
|
|
52
|
+
conversationId: z.ZodString;
|
|
53
|
+
posted: z.ZodString;
|
|
54
|
+
messageUrl: z.ZodString;
|
|
55
|
+
attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
56
|
+
attachmentId: z.ZodString;
|
|
57
|
+
urlType: z.ZodString;
|
|
58
|
+
title: z.ZodOptional<z.ZodString>;
|
|
59
|
+
url: z.ZodOptional<z.ZodString>;
|
|
60
|
+
fileName: z.ZodOptional<z.ZodString>;
|
|
61
|
+
fileSize: z.ZodOptional<z.ZodNumber>;
|
|
62
|
+
underlyingType: z.ZodOptional<z.ZodString>;
|
|
63
|
+
description: z.ZodOptional<z.ZodString>;
|
|
64
|
+
image: z.ZodOptional<z.ZodString>;
|
|
65
|
+
imageWidth: z.ZodOptional<z.ZodNumber>;
|
|
66
|
+
imageHeight: z.ZodOptional<z.ZodNumber>;
|
|
67
|
+
duration: z.ZodOptional<z.ZodString>;
|
|
68
|
+
uploadState: z.ZodOptional<z.ZodString>;
|
|
69
|
+
video: z.ZodOptional<z.ZodString>;
|
|
70
|
+
videoType: z.ZodOptional<z.ZodString>;
|
|
71
|
+
videoAutoPlay: z.ZodOptional<z.ZodBoolean>;
|
|
72
|
+
}, z.core.$loose>>>;
|
|
73
|
+
}, z.core.$strip>>;
|
|
74
|
+
totalMessages: z.ZodNumber;
|
|
75
|
+
};
|
|
76
|
+
annotations: {
|
|
77
|
+
readOnlyHint: true;
|
|
78
|
+
destructiveHint: false;
|
|
79
|
+
idempotentHint: true;
|
|
80
|
+
};
|
|
81
|
+
execute(args: {
|
|
82
|
+
conversationId: string;
|
|
83
|
+
limit: number;
|
|
84
|
+
includeParticipants: boolean;
|
|
85
|
+
newerThanDate?: string | undefined;
|
|
86
|
+
olderThanDate?: string | undefined;
|
|
87
|
+
}, client: import("@doist/comms-sdk").CommsApi): Promise<{
|
|
88
|
+
textContent: string;
|
|
89
|
+
structuredContent: LoadConversationStructured;
|
|
90
|
+
}>;
|
|
91
|
+
};
|
|
92
|
+
export { loadConversation, type LoadConversationStructured };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { getFullCommsURL } from '@doist/comms-sdk';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { formatAttachmentsLine, normalizeAttachments } from '../utils/attachments.js';
|
|
4
|
+
import { UNKNOWN_USER } from '../utils/constants.js';
|
|
5
|
+
import { resolveUsersWithLog } from '../utils/degrade.js';
|
|
6
|
+
import { LoadConversationOutputSchema } from '../utils/output-schemas.js';
|
|
7
|
+
import { ToolNames } from '../utils/tool-names.js';
|
|
8
|
+
const ArgsSchema = {
|
|
9
|
+
conversationId: z.string().describe('The conversation ID to load.'),
|
|
10
|
+
newerThanDate: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe('Get messages newer than this date (YYYY-MM-DD format).'),
|
|
14
|
+
olderThanDate: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('Get messages older than this date (YYYY-MM-DD format).'),
|
|
18
|
+
limit: z
|
|
19
|
+
.number()
|
|
20
|
+
.int()
|
|
21
|
+
.min(1)
|
|
22
|
+
.max(100)
|
|
23
|
+
.optional()
|
|
24
|
+
.default(50)
|
|
25
|
+
.describe('Maximum number of messages to return.'),
|
|
26
|
+
includeParticipants: z
|
|
27
|
+
.boolean()
|
|
28
|
+
.optional()
|
|
29
|
+
.default(true)
|
|
30
|
+
.describe('Include participant user IDs in the response.'),
|
|
31
|
+
};
|
|
32
|
+
const loadConversation = {
|
|
33
|
+
name: ToolNames.LOAD_CONVERSATION,
|
|
34
|
+
description: 'Load a conversation (direct message) with its metadata and messages. Supports filtering by timestamp and pagination.',
|
|
35
|
+
parameters: ArgsSchema,
|
|
36
|
+
outputSchema: LoadConversationOutputSchema.shape,
|
|
37
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
38
|
+
async execute(args, client) {
|
|
39
|
+
const { conversationId, newerThanDate, olderThanDate, limit, includeParticipants } = args;
|
|
40
|
+
// Fetch conversation metadata and messages in parallel
|
|
41
|
+
const [conversation, messages] = await Promise.all([
|
|
42
|
+
client.conversations.getConversation(conversationId),
|
|
43
|
+
client.conversationMessages.getMessages({
|
|
44
|
+
conversationId,
|
|
45
|
+
newerThan: newerThanDate ? new Date(newerThanDate) : undefined,
|
|
46
|
+
olderThan: olderThanDate ? new Date(olderThanDate) : undefined,
|
|
47
|
+
limit,
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
const { userIds } = conversation;
|
|
51
|
+
// A participant that cannot be resolved is dropped rather than failing the
|
|
52
|
+
// whole conversation; the message list matters more than every name.
|
|
53
|
+
const userInfo = await resolveUsersWithLog(client, ToolNames.LOAD_CONVERSATION, 'failed to resolve conversation participant', conversation.workspaceId, userIds);
|
|
54
|
+
// Build text content
|
|
55
|
+
const lines = [
|
|
56
|
+
`# Conversation ${conversationId}`,
|
|
57
|
+
'',
|
|
58
|
+
`**Conversation ID:** ${conversation.id}`,
|
|
59
|
+
...(conversation.title ? [`**Title:** ${conversation.title}`] : []),
|
|
60
|
+
`**Workspace ID:** ${conversation.workspaceId}`,
|
|
61
|
+
`**Archived:** ${conversation.archived ? 'Yes' : 'No'}`,
|
|
62
|
+
`**Last Active:** ${conversation.lastActive.toISOString()}`,
|
|
63
|
+
'',
|
|
64
|
+
];
|
|
65
|
+
if (includeParticipants) {
|
|
66
|
+
lines.push('## Participants');
|
|
67
|
+
lines.push('');
|
|
68
|
+
lines.push(conversation.userIds
|
|
69
|
+
.map((id) => userInfo.get(id)?.fullName ?? UNKNOWN_USER)
|
|
70
|
+
.join(', '));
|
|
71
|
+
lines.push('');
|
|
72
|
+
}
|
|
73
|
+
lines.push(`## Messages (${messages.length})`);
|
|
74
|
+
lines.push('');
|
|
75
|
+
// Normalised once per message, for both the text and the structured content.
|
|
76
|
+
const attachmentsByMessage = new Map(messages.map((m) => [m.id, normalizeAttachments(m.attachments)]));
|
|
77
|
+
for (const message of messages) {
|
|
78
|
+
const messageDate = message.posted.toISOString();
|
|
79
|
+
lines.push(`### Message ${message.id}`);
|
|
80
|
+
lines.push(`**Creator:** ${userInfo.get(message.creator)?.fullName ?? UNKNOWN_USER} | **Posted:** ${messageDate}`);
|
|
81
|
+
lines.push('');
|
|
82
|
+
lines.push(message.content);
|
|
83
|
+
const attachmentsLine = formatAttachmentsLine(attachmentsByMessage.get(message.id));
|
|
84
|
+
if (attachmentsLine) {
|
|
85
|
+
lines.push('');
|
|
86
|
+
lines.push(attachmentsLine);
|
|
87
|
+
}
|
|
88
|
+
lines.push('');
|
|
89
|
+
}
|
|
90
|
+
const structuredContent = {
|
|
91
|
+
type: 'conversation_data',
|
|
92
|
+
conversation: {
|
|
93
|
+
id: conversation.id,
|
|
94
|
+
title: conversation.title ?? undefined,
|
|
95
|
+
workspaceId: conversation.workspaceId,
|
|
96
|
+
userIds: includeParticipants ? conversation.userIds : [],
|
|
97
|
+
archived: conversation.archived,
|
|
98
|
+
lastActive: conversation.lastActive.toISOString(),
|
|
99
|
+
conversationUrl: conversation.url ??
|
|
100
|
+
getFullCommsURL({
|
|
101
|
+
workspaceId: conversation.workspaceId,
|
|
102
|
+
conversationId: conversation.id,
|
|
103
|
+
}),
|
|
104
|
+
},
|
|
105
|
+
messages: messages.map((m) => ({
|
|
106
|
+
id: m.id,
|
|
107
|
+
content: m.content,
|
|
108
|
+
creatorId: m.creator,
|
|
109
|
+
creatorName: userInfo.get(m.creator)?.fullName,
|
|
110
|
+
conversationId: m.conversationId,
|
|
111
|
+
posted: m.posted.toISOString(),
|
|
112
|
+
messageUrl: m.url ??
|
|
113
|
+
getFullCommsURL({
|
|
114
|
+
workspaceId: m.workspaceId,
|
|
115
|
+
conversationId: m.conversationId,
|
|
116
|
+
messageId: m.id,
|
|
117
|
+
}),
|
|
118
|
+
attachments: attachmentsByMessage.get(m.id),
|
|
119
|
+
})),
|
|
120
|
+
totalMessages: conversation.messageCount ?? 0,
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
textContent: lines.join('\n'),
|
|
124
|
+
structuredContent,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
export { loadConversation };
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type OutputAttachment } from '../utils/output-schemas.js';
|
|
3
|
+
type LoadThreadStructured = {
|
|
4
|
+
type: 'thread_data';
|
|
5
|
+
thread: {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
content: string;
|
|
9
|
+
channelId: string;
|
|
10
|
+
channelName?: string;
|
|
11
|
+
workspaceId: number;
|
|
12
|
+
creator: number;
|
|
13
|
+
creatorName?: string;
|
|
14
|
+
posted: string;
|
|
15
|
+
commentCount: number;
|
|
16
|
+
isArchived: boolean;
|
|
17
|
+
inInbox: boolean;
|
|
18
|
+
participants?: number[];
|
|
19
|
+
participantNames?: string[];
|
|
20
|
+
threadUrl: string;
|
|
21
|
+
attachments?: OutputAttachment[];
|
|
22
|
+
};
|
|
23
|
+
comments: Array<{
|
|
24
|
+
id: string;
|
|
25
|
+
content: string;
|
|
26
|
+
creator: number;
|
|
27
|
+
creatorName?: string;
|
|
28
|
+
threadId: string;
|
|
29
|
+
posted: string;
|
|
30
|
+
commentUrl: string;
|
|
31
|
+
attachments?: OutputAttachment[];
|
|
32
|
+
}>;
|
|
33
|
+
totalComments: number;
|
|
34
|
+
};
|
|
35
|
+
declare const loadThread: {
|
|
36
|
+
name: "load-thread";
|
|
37
|
+
description: string;
|
|
38
|
+
parameters: {
|
|
39
|
+
threadId: z.ZodString;
|
|
40
|
+
newerThanDate: z.ZodOptional<z.ZodString>;
|
|
41
|
+
olderThanDate: z.ZodOptional<z.ZodString>;
|
|
42
|
+
limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
43
|
+
includeParticipants: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
44
|
+
};
|
|
45
|
+
outputSchema: {
|
|
46
|
+
type: z.ZodLiteral<"thread_data">;
|
|
47
|
+
thread: z.ZodObject<{
|
|
48
|
+
id: z.ZodString;
|
|
49
|
+
title: z.ZodString;
|
|
50
|
+
content: z.ZodString;
|
|
51
|
+
channelId: z.ZodString;
|
|
52
|
+
channelName: z.ZodOptional<z.ZodString>;
|
|
53
|
+
workspaceId: z.ZodNumber;
|
|
54
|
+
creator: z.ZodNumber;
|
|
55
|
+
creatorName: z.ZodOptional<z.ZodString>;
|
|
56
|
+
posted: z.ZodString;
|
|
57
|
+
commentCount: z.ZodNumber;
|
|
58
|
+
isArchived: z.ZodBoolean;
|
|
59
|
+
inInbox: z.ZodBoolean;
|
|
60
|
+
participants: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
|
|
61
|
+
participantNames: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
62
|
+
threadUrl: z.ZodString;
|
|
63
|
+
attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
64
|
+
attachmentId: z.ZodString;
|
|
65
|
+
urlType: z.ZodString;
|
|
66
|
+
title: z.ZodOptional<z.ZodString>;
|
|
67
|
+
url: z.ZodOptional<z.ZodString>;
|
|
68
|
+
fileName: z.ZodOptional<z.ZodString>;
|
|
69
|
+
fileSize: z.ZodOptional<z.ZodNumber>;
|
|
70
|
+
underlyingType: z.ZodOptional<z.ZodString>;
|
|
71
|
+
description: z.ZodOptional<z.ZodString>;
|
|
72
|
+
image: z.ZodOptional<z.ZodString>;
|
|
73
|
+
imageWidth: z.ZodOptional<z.ZodNumber>;
|
|
74
|
+
imageHeight: z.ZodOptional<z.ZodNumber>;
|
|
75
|
+
duration: z.ZodOptional<z.ZodString>;
|
|
76
|
+
uploadState: z.ZodOptional<z.ZodString>;
|
|
77
|
+
video: z.ZodOptional<z.ZodString>;
|
|
78
|
+
videoType: z.ZodOptional<z.ZodString>;
|
|
79
|
+
videoAutoPlay: z.ZodOptional<z.ZodBoolean>;
|
|
80
|
+
}, z.core.$loose>>>;
|
|
81
|
+
}, z.core.$strip>;
|
|
82
|
+
comments: z.ZodArray<z.ZodObject<{
|
|
83
|
+
id: z.ZodString;
|
|
84
|
+
content: z.ZodString;
|
|
85
|
+
creator: z.ZodNumber;
|
|
86
|
+
creatorName: z.ZodOptional<z.ZodString>;
|
|
87
|
+
threadId: z.ZodString;
|
|
88
|
+
posted: z.ZodString;
|
|
89
|
+
commentUrl: z.ZodString;
|
|
90
|
+
attachments: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
91
|
+
attachmentId: z.ZodString;
|
|
92
|
+
urlType: z.ZodString;
|
|
93
|
+
title: z.ZodOptional<z.ZodString>;
|
|
94
|
+
url: z.ZodOptional<z.ZodString>;
|
|
95
|
+
fileName: z.ZodOptional<z.ZodString>;
|
|
96
|
+
fileSize: z.ZodOptional<z.ZodNumber>;
|
|
97
|
+
underlyingType: z.ZodOptional<z.ZodString>;
|
|
98
|
+
description: z.ZodOptional<z.ZodString>;
|
|
99
|
+
image: z.ZodOptional<z.ZodString>;
|
|
100
|
+
imageWidth: z.ZodOptional<z.ZodNumber>;
|
|
101
|
+
imageHeight: z.ZodOptional<z.ZodNumber>;
|
|
102
|
+
duration: z.ZodOptional<z.ZodString>;
|
|
103
|
+
uploadState: z.ZodOptional<z.ZodString>;
|
|
104
|
+
video: z.ZodOptional<z.ZodString>;
|
|
105
|
+
videoType: z.ZodOptional<z.ZodString>;
|
|
106
|
+
videoAutoPlay: z.ZodOptional<z.ZodBoolean>;
|
|
107
|
+
}, z.core.$loose>>>;
|
|
108
|
+
}, z.core.$strip>>;
|
|
109
|
+
totalComments: z.ZodNumber;
|
|
110
|
+
};
|
|
111
|
+
annotations: {
|
|
112
|
+
readOnlyHint: true;
|
|
113
|
+
destructiveHint: false;
|
|
114
|
+
idempotentHint: true;
|
|
115
|
+
};
|
|
116
|
+
execute(args: {
|
|
117
|
+
threadId: string;
|
|
118
|
+
limit: number;
|
|
119
|
+
includeParticipants: boolean;
|
|
120
|
+
newerThanDate?: string | undefined;
|
|
121
|
+
olderThanDate?: string | undefined;
|
|
122
|
+
}, client: import("@doist/comms-sdk").CommsApi): Promise<{
|
|
123
|
+
textContent: string;
|
|
124
|
+
structuredContent: LoadThreadStructured;
|
|
125
|
+
}>;
|
|
126
|
+
};
|
|
127
|
+
export { loadThread, type LoadThreadStructured };
|