@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,175 @@
|
|
|
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 { LoadThreadOutputSchema } from '../utils/output-schemas.js';
|
|
7
|
+
import { ToolNames } from '../utils/tool-names.js';
|
|
8
|
+
const ArgsSchema = {
|
|
9
|
+
threadId: z.string().describe('The thread ID to load.'),
|
|
10
|
+
newerThanDate: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe('Get comments newer than this date (YYYY-MM-DD format).'),
|
|
14
|
+
olderThanDate: z
|
|
15
|
+
.string()
|
|
16
|
+
.optional()
|
|
17
|
+
.describe('Get comments 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 comments to return.'),
|
|
26
|
+
includeParticipants: z
|
|
27
|
+
.boolean()
|
|
28
|
+
.optional()
|
|
29
|
+
.default(true)
|
|
30
|
+
.describe('Include participant user IDs in the response.'),
|
|
31
|
+
};
|
|
32
|
+
const loadThread = {
|
|
33
|
+
name: ToolNames.LOAD_THREAD,
|
|
34
|
+
description: 'Load a thread with its metadata and comments. Supports filtering by timestamp and pagination.',
|
|
35
|
+
parameters: ArgsSchema,
|
|
36
|
+
outputSchema: LoadThreadOutputSchema.shape,
|
|
37
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
|
|
38
|
+
async execute(args, client) {
|
|
39
|
+
const { threadId, newerThanDate, olderThanDate, limit, includeParticipants } = args;
|
|
40
|
+
// Fetch thread metadata and comments in parallel
|
|
41
|
+
const [thread, comments] = await Promise.all([
|
|
42
|
+
client.threads.getThread(threadId),
|
|
43
|
+
client.comments.getComments({
|
|
44
|
+
threadId,
|
|
45
|
+
newerThan: newerThanDate ? new Date(newerThanDate) : undefined,
|
|
46
|
+
olderThan: olderThanDate ? new Date(olderThanDate) : undefined,
|
|
47
|
+
limit,
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
// Collect all unique user IDs
|
|
51
|
+
const userIds = new Set([thread.creator]);
|
|
52
|
+
for (const comment of comments) {
|
|
53
|
+
userIds.add(comment.creator);
|
|
54
|
+
}
|
|
55
|
+
if (includeParticipants && thread.participants) {
|
|
56
|
+
for (const participantId of thread.participants) {
|
|
57
|
+
userIds.add(participantId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Fetch channel and all user info in parallel
|
|
61
|
+
const [channel, users] = await Promise.all([
|
|
62
|
+
client.channels.getChannel(thread.channelId),
|
|
63
|
+
// IDs that can't be resolved are simply absent from the lookup, so a
|
|
64
|
+
// single inaccessible user doesn't fail the whole thread.
|
|
65
|
+
resolveUsersWithLog(client, ToolNames.LOAD_THREAD, 'failed to resolve thread participant', thread.workspaceId, Array.from(userIds)),
|
|
66
|
+
]);
|
|
67
|
+
const userLookup = {};
|
|
68
|
+
for (const [id, user] of users) {
|
|
69
|
+
userLookup[id] = user.fullName;
|
|
70
|
+
}
|
|
71
|
+
// Build text content
|
|
72
|
+
const creatorName = userLookup[thread.creator];
|
|
73
|
+
const lines = [
|
|
74
|
+
`# Thread: ${thread.title}`,
|
|
75
|
+
'',
|
|
76
|
+
`**Thread ID:** ${thread.id}`,
|
|
77
|
+
`**Channel:** ${channel.name}`,
|
|
78
|
+
`**Workspace ID:** ${thread.workspaceId}`,
|
|
79
|
+
`**Creator:** ${creatorName ?? UNKNOWN_USER} (${thread.creator})`,
|
|
80
|
+
`**Posted:** ${thread.posted.toISOString()}`,
|
|
81
|
+
`**Comments:** ${thread.commentCount}`,
|
|
82
|
+
`**Archived:** ${thread.isArchived ? 'Yes' : 'No'}`,
|
|
83
|
+
`**In Inbox:** ${thread.inInbox ? 'Yes' : 'No'}`,
|
|
84
|
+
'',
|
|
85
|
+
'## Content',
|
|
86
|
+
'',
|
|
87
|
+
thread.content,
|
|
88
|
+
'',
|
|
89
|
+
];
|
|
90
|
+
// Normalised once per thread and comment, for both the text and the structured content.
|
|
91
|
+
const threadAttachments = normalizeAttachments(thread.attachments);
|
|
92
|
+
const attachmentsByComment = new Map(comments.map((c) => [c.id, normalizeAttachments(c.attachments)]));
|
|
93
|
+
const threadAttachmentsLine = formatAttachmentsLine(threadAttachments);
|
|
94
|
+
if (threadAttachmentsLine) {
|
|
95
|
+
lines.push(threadAttachmentsLine);
|
|
96
|
+
lines.push('');
|
|
97
|
+
}
|
|
98
|
+
lines.push(`## Comments (${comments.length})`);
|
|
99
|
+
lines.push('');
|
|
100
|
+
for (const comment of comments) {
|
|
101
|
+
const commentDate = comment.posted.toISOString();
|
|
102
|
+
const commentCreatorName = userLookup[comment.creator];
|
|
103
|
+
lines.push(`### Comment ${comment.id}`);
|
|
104
|
+
lines.push(`**Creator:** ${commentCreatorName ?? UNKNOWN_USER} (${comment.creator}) | **Posted:** ${commentDate}`);
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push(comment.content);
|
|
107
|
+
const commentAttachmentsLine = formatAttachmentsLine(attachmentsByComment.get(comment.id));
|
|
108
|
+
if (commentAttachmentsLine) {
|
|
109
|
+
lines.push('');
|
|
110
|
+
lines.push(commentAttachmentsLine);
|
|
111
|
+
}
|
|
112
|
+
lines.push('');
|
|
113
|
+
}
|
|
114
|
+
if (includeParticipants && thread.participants) {
|
|
115
|
+
lines.push('## Participants');
|
|
116
|
+
lines.push('');
|
|
117
|
+
const participantList = thread.participants
|
|
118
|
+
.map((id) => `${userLookup[id] ?? UNKNOWN_USER} (${id})`)
|
|
119
|
+
.join(', ');
|
|
120
|
+
lines.push(participantList);
|
|
121
|
+
}
|
|
122
|
+
const structuredContent = {
|
|
123
|
+
type: 'thread_data',
|
|
124
|
+
thread: {
|
|
125
|
+
id: thread.id,
|
|
126
|
+
title: thread.title,
|
|
127
|
+
content: thread.content,
|
|
128
|
+
channelId: thread.channelId,
|
|
129
|
+
channelName: channel.name,
|
|
130
|
+
workspaceId: thread.workspaceId,
|
|
131
|
+
creator: thread.creator,
|
|
132
|
+
creatorName,
|
|
133
|
+
posted: thread.posted.toISOString(),
|
|
134
|
+
commentCount: thread.commentCount,
|
|
135
|
+
isArchived: thread.isArchived,
|
|
136
|
+
inInbox: thread.inInbox ?? false,
|
|
137
|
+
participants: includeParticipants ? (thread.participants ?? undefined) : undefined,
|
|
138
|
+
participantNames: includeParticipants && thread.participants
|
|
139
|
+
? thread.participants
|
|
140
|
+
.map((id) => userLookup[id])
|
|
141
|
+
.filter((name) => name !== undefined)
|
|
142
|
+
: undefined,
|
|
143
|
+
threadUrl: thread.url ??
|
|
144
|
+
getFullCommsURL({
|
|
145
|
+
workspaceId: thread.workspaceId,
|
|
146
|
+
channelId: thread.channelId,
|
|
147
|
+
threadId: thread.id,
|
|
148
|
+
}),
|
|
149
|
+
attachments: threadAttachments,
|
|
150
|
+
},
|
|
151
|
+
comments: comments.map((c) => ({
|
|
152
|
+
id: c.id,
|
|
153
|
+
content: c.content,
|
|
154
|
+
creator: c.creator,
|
|
155
|
+
creatorName: userLookup[c.creator],
|
|
156
|
+
threadId: c.threadId,
|
|
157
|
+
posted: c.posted.toISOString(),
|
|
158
|
+
commentUrl: c.url ??
|
|
159
|
+
getFullCommsURL({
|
|
160
|
+
workspaceId: c.workspaceId,
|
|
161
|
+
channelId: c.channelId,
|
|
162
|
+
threadId: c.threadId,
|
|
163
|
+
commentId: c.id,
|
|
164
|
+
}),
|
|
165
|
+
attachments: attachmentsByComment.get(c.id),
|
|
166
|
+
})),
|
|
167
|
+
totalComments: thread.commentCount,
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
textContent: lines.join('\n'),
|
|
171
|
+
structuredContent,
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
export { loadThread };
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type MarkDoneOp } from '../utils/output-schemas.js';
|
|
3
|
+
import { type MarkDoneType } from '../utils/target-types.js';
|
|
4
|
+
type MarkDoneStructured = {
|
|
5
|
+
type: 'mark_done_result';
|
|
6
|
+
itemType: MarkDoneType;
|
|
7
|
+
mode: 'individual' | 'bulk';
|
|
8
|
+
completed: string[];
|
|
9
|
+
failed: Array<{
|
|
10
|
+
item: string;
|
|
11
|
+
error: string;
|
|
12
|
+
opErrors: Array<{
|
|
13
|
+
op: MarkDoneOp;
|
|
14
|
+
error: string;
|
|
15
|
+
}>;
|
|
16
|
+
}>;
|
|
17
|
+
warnings: Array<{
|
|
18
|
+
item: string;
|
|
19
|
+
op: MarkDoneOp;
|
|
20
|
+
error: string;
|
|
21
|
+
}>;
|
|
22
|
+
totalRequested: number;
|
|
23
|
+
successCount: number;
|
|
24
|
+
failureCount: number;
|
|
25
|
+
operations: {
|
|
26
|
+
markRead: boolean;
|
|
27
|
+
archive: boolean;
|
|
28
|
+
clearUnread: boolean;
|
|
29
|
+
};
|
|
30
|
+
selectors?: {
|
|
31
|
+
workspaceId?: number;
|
|
32
|
+
channelId?: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
declare const markDone: {
|
|
36
|
+
name: "mark-done";
|
|
37
|
+
description: string;
|
|
38
|
+
parameters: {
|
|
39
|
+
type: z.ZodEnum<{
|
|
40
|
+
thread: "thread";
|
|
41
|
+
conversation: "conversation";
|
|
42
|
+
}>;
|
|
43
|
+
ids: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
44
|
+
workspaceId: z.ZodOptional<z.ZodNumber>;
|
|
45
|
+
channelId: z.ZodOptional<z.ZodString>;
|
|
46
|
+
markRead: z.ZodOptional<z.ZodBoolean>;
|
|
47
|
+
archive: z.ZodOptional<z.ZodBoolean>;
|
|
48
|
+
clearUnread: z.ZodOptional<z.ZodBoolean>;
|
|
49
|
+
};
|
|
50
|
+
outputSchema: {
|
|
51
|
+
type: z.ZodLiteral<"mark_done_result">;
|
|
52
|
+
itemType: z.ZodEnum<{
|
|
53
|
+
thread: "thread";
|
|
54
|
+
conversation: "conversation";
|
|
55
|
+
}>;
|
|
56
|
+
mode: z.ZodEnum<{
|
|
57
|
+
individual: "individual";
|
|
58
|
+
bulk: "bulk";
|
|
59
|
+
}>;
|
|
60
|
+
completed: z.ZodArray<z.ZodString>;
|
|
61
|
+
failed: z.ZodArray<z.ZodObject<{
|
|
62
|
+
item: z.ZodString;
|
|
63
|
+
error: z.ZodString;
|
|
64
|
+
opErrors: z.ZodArray<z.ZodObject<{
|
|
65
|
+
op: z.ZodEnum<{
|
|
66
|
+
markRead: "markRead";
|
|
67
|
+
archive: "archive";
|
|
68
|
+
}>;
|
|
69
|
+
error: z.ZodString;
|
|
70
|
+
}, z.core.$strip>>;
|
|
71
|
+
}, z.core.$strip>>;
|
|
72
|
+
warnings: z.ZodArray<z.ZodObject<{
|
|
73
|
+
item: z.ZodString;
|
|
74
|
+
op: z.ZodEnum<{
|
|
75
|
+
markRead: "markRead";
|
|
76
|
+
archive: "archive";
|
|
77
|
+
}>;
|
|
78
|
+
error: z.ZodString;
|
|
79
|
+
}, z.core.$strip>>;
|
|
80
|
+
totalRequested: z.ZodNumber;
|
|
81
|
+
successCount: z.ZodNumber;
|
|
82
|
+
failureCount: z.ZodNumber;
|
|
83
|
+
operations: z.ZodObject<{
|
|
84
|
+
markRead: z.ZodBoolean;
|
|
85
|
+
archive: z.ZodBoolean;
|
|
86
|
+
clearUnread: z.ZodBoolean;
|
|
87
|
+
}, z.core.$strip>;
|
|
88
|
+
selectors: z.ZodOptional<z.ZodObject<{
|
|
89
|
+
workspaceId: z.ZodOptional<z.ZodNumber>;
|
|
90
|
+
channelId: z.ZodOptional<z.ZodString>;
|
|
91
|
+
}, z.core.$strip>>;
|
|
92
|
+
};
|
|
93
|
+
annotations: {
|
|
94
|
+
readOnlyHint: false;
|
|
95
|
+
destructiveHint: true;
|
|
96
|
+
idempotentHint: true;
|
|
97
|
+
};
|
|
98
|
+
execute(args: {
|
|
99
|
+
type: "thread" | "conversation";
|
|
100
|
+
ids?: string[] | undefined;
|
|
101
|
+
workspaceId?: number | undefined;
|
|
102
|
+
channelId?: string | undefined;
|
|
103
|
+
markRead?: boolean | undefined;
|
|
104
|
+
archive?: boolean | undefined;
|
|
105
|
+
clearUnread?: boolean | undefined;
|
|
106
|
+
}, client: import("@doist/comms-sdk").CommsApi): Promise<{
|
|
107
|
+
textContent: string;
|
|
108
|
+
structuredContent: MarkDoneStructured;
|
|
109
|
+
}>;
|
|
110
|
+
};
|
|
111
|
+
export { markDone, type MarkDoneStructured };
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { limitedAll } from '../utils/concurrency.js';
|
|
3
|
+
import { logOperationFailures } from '../utils/degrade.js';
|
|
4
|
+
import { MarkDoneOutputSchema } from '../utils/output-schemas.js';
|
|
5
|
+
import { markConversationFullyRead, markThreadFullyRead } from '../utils/read-position.js';
|
|
6
|
+
import { MarkDoneTypeSchema } from '../utils/target-types.js';
|
|
7
|
+
import { ToolNames } from '../utils/tool-names.js';
|
|
8
|
+
const ArgsSchema = {
|
|
9
|
+
type: MarkDoneTypeSchema.describe('The type of items to mark as done: thread or conversation.'),
|
|
10
|
+
// Individual IDs (thread/conversation IDs are strings)
|
|
11
|
+
ids: z
|
|
12
|
+
.array(z.string())
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('Specific thread or conversation IDs to mark as done. Use this OR bulk selectors.'),
|
|
15
|
+
// Bulk selectors (for threads only)
|
|
16
|
+
workspaceId: z
|
|
17
|
+
.number()
|
|
18
|
+
.optional()
|
|
19
|
+
.describe('Mark all threads in this workspace as done (threads only).'),
|
|
20
|
+
channelId: z
|
|
21
|
+
.string()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe('Mark all threads in this channel as done (threads only).'),
|
|
24
|
+
// Operations
|
|
25
|
+
markRead: z.boolean().optional().describe('Mark items as read (default: true).'),
|
|
26
|
+
archive: z
|
|
27
|
+
.boolean()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe('Archive items in the inbox (threads only, default: true).'),
|
|
30
|
+
clearUnread: z
|
|
31
|
+
.boolean()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('Clear all unread markers for workspace (threads only, requires workspaceId, default: false).'),
|
|
34
|
+
};
|
|
35
|
+
const markDone = {
|
|
36
|
+
name: ToolNames.MARK_DONE,
|
|
37
|
+
description: 'Mark threads or conversations as done. Supports individual IDs or bulk operations (mark all in workspace/channel). For threads: can mark as read, archive in inbox, or clear all unread. For conversations: can mark as read and archive.',
|
|
38
|
+
parameters: ArgsSchema,
|
|
39
|
+
outputSchema: MarkDoneOutputSchema.shape,
|
|
40
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true },
|
|
41
|
+
async execute(args, client) {
|
|
42
|
+
const { type, ids, workspaceId, channelId, markRead = true, archive = true, clearUnread = false, } = args;
|
|
43
|
+
const completed = [];
|
|
44
|
+
const failed = [];
|
|
45
|
+
const warnings = [];
|
|
46
|
+
let mode = 'individual';
|
|
47
|
+
// Validate arguments
|
|
48
|
+
if (!ids && !workspaceId && !channelId) {
|
|
49
|
+
throw new Error('Must provide either ids, workspaceId, or channelId');
|
|
50
|
+
}
|
|
51
|
+
if (type === 'conversation' && (workspaceId || channelId || clearUnread)) {
|
|
52
|
+
throw new Error('Bulk operations (workspaceId, channelId, clearUnread) are only supported for threads');
|
|
53
|
+
}
|
|
54
|
+
// Reject channel-only bulk archive up front: the SDK's `inbox.archiveAll`
|
|
55
|
+
// requires a workspaceId, so without one we'd silently drop the archive
|
|
56
|
+
// step while reporting success — exactly the "lie about state" failure
|
|
57
|
+
// mode this tool must avoid.
|
|
58
|
+
if (type === 'thread' && archive && channelId && !workspaceId) {
|
|
59
|
+
throw new Error('Archiving by channelId requires workspaceId. Pass both, or pass individual `ids` instead.');
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
// Bulk operations (threads only)
|
|
63
|
+
if (type === 'thread' && (workspaceId || channelId)) {
|
|
64
|
+
mode = 'bulk';
|
|
65
|
+
// Clear unread takes precedence; it's strictly workspace-scoped
|
|
66
|
+
// (the SDK only exposes a workspace-level signature), so we
|
|
67
|
+
// require workspaceId for it.
|
|
68
|
+
if (clearUnread && workspaceId) {
|
|
69
|
+
await client.threads.clearUnread(workspaceId);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Mark all read — pass both selectors when both are present
|
|
73
|
+
// so the call stays scoped to the channel inside the
|
|
74
|
+
// workspace rather than nuking the whole workspace.
|
|
75
|
+
if (markRead) {
|
|
76
|
+
if (workspaceId && channelId) {
|
|
77
|
+
await client.threads.markAllRead({ workspaceId, channelId });
|
|
78
|
+
}
|
|
79
|
+
else if (workspaceId) {
|
|
80
|
+
await client.threads.markAllRead({ workspaceId });
|
|
81
|
+
}
|
|
82
|
+
else if (channelId) {
|
|
83
|
+
await client.threads.markAllRead({ channelId });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Archive all (inbox operations). `archiveAll` requires a
|
|
87
|
+
// workspaceId; with a channelId also present, pass it via
|
|
88
|
+
// `channelIds` so the archive stays scoped to that channel.
|
|
89
|
+
// The channel-only case is rejected up front in the
|
|
90
|
+
// validation above so this branch is reachable only with
|
|
91
|
+
// a workspaceId.
|
|
92
|
+
if (archive && workspaceId) {
|
|
93
|
+
if (channelId) {
|
|
94
|
+
await client.inbox.archiveAll({
|
|
95
|
+
workspaceId,
|
|
96
|
+
channelIds: [channelId],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
await client.inbox.archiveAll({ workspaceId });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// We don't get individual IDs back from bulk operations
|
|
105
|
+
// Just indicate success
|
|
106
|
+
}
|
|
107
|
+
else if (ids && ids.length > 0) {
|
|
108
|
+
// Individual operations - run them in parallel
|
|
109
|
+
mode = 'individual';
|
|
110
|
+
// Run each requested operation independently so a failure in
|
|
111
|
+
// one (e.g. markRead) never prevents the other (e.g. archive)
|
|
112
|
+
// from running. The `ids` list is user-supplied so it can be
|
|
113
|
+
// large — `limitedAll` keeps the burst inside the socket pool /
|
|
114
|
+
// rate limiter.
|
|
115
|
+
const results = await limitedAll(ids, async (id) => {
|
|
116
|
+
const opErrors = [];
|
|
117
|
+
const runOp = async (op, fn) => {
|
|
118
|
+
try {
|
|
119
|
+
await fn();
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
opErrors.push({
|
|
123
|
+
op,
|
|
124
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
if (type === 'thread') {
|
|
129
|
+
if (markRead) {
|
|
130
|
+
await runOp('markRead', () => markThreadFullyRead(client, id));
|
|
131
|
+
}
|
|
132
|
+
if (archive) {
|
|
133
|
+
await runOp('archive', () => client.inbox.archiveThread(id));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
if (markRead) {
|
|
138
|
+
await runOp('markRead', () => markConversationFullyRead(client, id));
|
|
139
|
+
}
|
|
140
|
+
if (archive) {
|
|
141
|
+
await runOp('archive', () => client.conversations.archiveConversation(id));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { id, opErrors };
|
|
145
|
+
});
|
|
146
|
+
// "Done" is defined by the archive operation when requested;
|
|
147
|
+
// otherwise by markRead. If that operation succeeds the item is
|
|
148
|
+
// completed even if a secondary operation failed — the failure
|
|
149
|
+
// is surfaced as a non-fatal warning rather than failing the
|
|
150
|
+
// whole item (and breaking automations whose goal is archiving).
|
|
151
|
+
const doneOp = archive ? 'archive' : 'markRead';
|
|
152
|
+
for (const r of results) {
|
|
153
|
+
const doneFailure = r.opErrors.find((e) => e.op === doneOp);
|
|
154
|
+
if (doneFailure) {
|
|
155
|
+
const detail = r.opErrors.map((e) => `${e.op}: ${e.error}`).join('; ');
|
|
156
|
+
failed.push({ item: r.id, error: detail, opErrors: r.opErrors });
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
completed.push(r.id);
|
|
160
|
+
for (const e of r.opErrors) {
|
|
161
|
+
warnings.push({ item: r.id, op: e.op, error: e.error });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
logOperationFailures(ToolNames.MARK_DONE, failed.map(({ item, error }) => ({ item, error })), { warnings, context: { itemType: type } });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
// Bulk operation failed entirely
|
|
170
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
171
|
+
throw new Error(`Bulk operation failed: ${errorMessage}`);
|
|
172
|
+
}
|
|
173
|
+
// Build text content
|
|
174
|
+
const lines = [
|
|
175
|
+
`# Mark ${type === 'thread' ? 'Threads' : 'Conversations'} Done`,
|
|
176
|
+
'',
|
|
177
|
+
];
|
|
178
|
+
lines.push(`**Mode:** ${mode === 'bulk' ? 'Bulk Operation' : 'Individual IDs'}`);
|
|
179
|
+
if (mode === 'bulk') {
|
|
180
|
+
if (workspaceId) {
|
|
181
|
+
lines.push(`**Workspace ID:** ${workspaceId}`);
|
|
182
|
+
}
|
|
183
|
+
if (channelId) {
|
|
184
|
+
lines.push(`**Channel ID:** ${channelId}`);
|
|
185
|
+
}
|
|
186
|
+
if (clearUnread) {
|
|
187
|
+
lines.push('**Operation:** Clear all unread markers');
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
lines.push(`**Mark Read:** ${markRead ? 'Yes' : 'No'}`);
|
|
191
|
+
lines.push(`**Archive:** ${archive ? 'Yes' : 'No'}`);
|
|
192
|
+
}
|
|
193
|
+
lines.push('');
|
|
194
|
+
lines.push('Bulk operation completed successfully');
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
lines.push(`**Total Requested:** ${ids?.length ?? 0}`);
|
|
198
|
+
lines.push(`**Successful:** ${completed.length}`);
|
|
199
|
+
lines.push(`**Failed:** ${failed.length}`);
|
|
200
|
+
if (warnings.length > 0) {
|
|
201
|
+
lines.push(`**Warnings:** ${warnings.length}`);
|
|
202
|
+
}
|
|
203
|
+
lines.push(`**Mark Read:** ${markRead ? 'Yes' : 'No'}`);
|
|
204
|
+
lines.push(`**Archive:** ${archive ? 'Yes' : 'No'}`);
|
|
205
|
+
lines.push('');
|
|
206
|
+
if (completed.length > 0) {
|
|
207
|
+
lines.push('## Completed');
|
|
208
|
+
lines.push('');
|
|
209
|
+
lines.push(completed.join(', '));
|
|
210
|
+
lines.push('');
|
|
211
|
+
}
|
|
212
|
+
if (failed.length > 0) {
|
|
213
|
+
lines.push('## Failed');
|
|
214
|
+
lines.push('');
|
|
215
|
+
for (const failure of failed) {
|
|
216
|
+
lines.push(`- ${type} ${failure.item}: ${failure.error}`);
|
|
217
|
+
}
|
|
218
|
+
lines.push('');
|
|
219
|
+
}
|
|
220
|
+
if (warnings.length > 0) {
|
|
221
|
+
lines.push('## Warnings');
|
|
222
|
+
lines.push('');
|
|
223
|
+
lines.push(`These ${type}s were marked done (archived), but a secondary operation failed:`);
|
|
224
|
+
for (const warning of warnings) {
|
|
225
|
+
lines.push(`- ${type} ${warning.item} (${warning.op}): ${warning.error}`);
|
|
226
|
+
}
|
|
227
|
+
lines.push('');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// Add next steps
|
|
231
|
+
lines.push('## Next Steps');
|
|
232
|
+
lines.push('');
|
|
233
|
+
if (mode === 'bulk' || (failed.length === 0 && completed.length > 0)) {
|
|
234
|
+
lines.push(type === 'thread'
|
|
235
|
+
? 'Use `fetch-inbox` to see remaining unread threads.'
|
|
236
|
+
: 'Check your conversations for remaining unread messages.');
|
|
237
|
+
}
|
|
238
|
+
else if (failed.length > 0) {
|
|
239
|
+
lines.push('Review failed items and retry if needed.');
|
|
240
|
+
}
|
|
241
|
+
// Warning items are archived but still unread. `fetch-inbox` defaults to
|
|
242
|
+
// `archiveFilter: "active"` and would hide them, so point callers at the
|
|
243
|
+
// archived view when they need to inspect or re-read those threads.
|
|
244
|
+
if (warnings.length > 0 && type === 'thread') {
|
|
245
|
+
lines.push('Warning threads are archived but still unread — use `fetch-inbox` with `archiveFilter: "all"` or `"archived"` to inspect them.');
|
|
246
|
+
}
|
|
247
|
+
const structuredContent = {
|
|
248
|
+
type: 'mark_done_result',
|
|
249
|
+
itemType: type,
|
|
250
|
+
mode,
|
|
251
|
+
completed,
|
|
252
|
+
failed,
|
|
253
|
+
warnings,
|
|
254
|
+
totalRequested: ids?.length ?? 0,
|
|
255
|
+
successCount: completed.length,
|
|
256
|
+
failureCount: failed.length,
|
|
257
|
+
operations: {
|
|
258
|
+
markRead,
|
|
259
|
+
archive,
|
|
260
|
+
clearUnread,
|
|
261
|
+
},
|
|
262
|
+
selectors: workspaceId || channelId
|
|
263
|
+
? {
|
|
264
|
+
workspaceId,
|
|
265
|
+
channelId,
|
|
266
|
+
}
|
|
267
|
+
: undefined,
|
|
268
|
+
};
|
|
269
|
+
return {
|
|
270
|
+
textContent: lines.join('\n'),
|
|
271
|
+
structuredContent,
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
export { markDone };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { CommsApi } from '@doist/comms-sdk';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { MarkReadItemType } from '../utils/target-types.js';
|
|
4
|
+
type MarkReadItems = {
|
|
5
|
+
marked: string[];
|
|
6
|
+
alreadyRead: string[];
|
|
7
|
+
};
|
|
8
|
+
type MarkReadFailure = {
|
|
9
|
+
item: string;
|
|
10
|
+
itemType: MarkReadItemType;
|
|
11
|
+
error: string;
|
|
12
|
+
};
|
|
13
|
+
type MarkReadStructured = {
|
|
14
|
+
type: 'mark_read_result';
|
|
15
|
+
workspaceId: number;
|
|
16
|
+
mode: 'individual' | 'all';
|
|
17
|
+
threads: MarkReadItems;
|
|
18
|
+
conversations: MarkReadItems;
|
|
19
|
+
failed: MarkReadFailure[];
|
|
20
|
+
markedCount: number;
|
|
21
|
+
failureCount: number;
|
|
22
|
+
};
|
|
23
|
+
declare const markRead: {
|
|
24
|
+
name: "mark-read";
|
|
25
|
+
description: string;
|
|
26
|
+
parameters: {
|
|
27
|
+
workspaceId: z.ZodNumber;
|
|
28
|
+
threadIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
29
|
+
conversationIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
30
|
+
all: z.ZodOptional<z.ZodBoolean>;
|
|
31
|
+
};
|
|
32
|
+
outputSchema: {
|
|
33
|
+
type: z.ZodLiteral<"mark_read_result">;
|
|
34
|
+
workspaceId: z.ZodNumber;
|
|
35
|
+
mode: z.ZodEnum<{
|
|
36
|
+
individual: "individual";
|
|
37
|
+
all: "all";
|
|
38
|
+
}>;
|
|
39
|
+
threads: z.ZodObject<{
|
|
40
|
+
marked: z.ZodArray<z.ZodString>;
|
|
41
|
+
alreadyRead: z.ZodArray<z.ZodString>;
|
|
42
|
+
}, z.core.$strip>;
|
|
43
|
+
conversations: z.ZodObject<{
|
|
44
|
+
marked: z.ZodArray<z.ZodString>;
|
|
45
|
+
alreadyRead: z.ZodArray<z.ZodString>;
|
|
46
|
+
}, z.core.$strip>;
|
|
47
|
+
failed: z.ZodArray<z.ZodObject<{
|
|
48
|
+
item: z.ZodString;
|
|
49
|
+
itemType: z.ZodEnum<{
|
|
50
|
+
thread: "thread";
|
|
51
|
+
conversation: "conversation";
|
|
52
|
+
}>;
|
|
53
|
+
error: z.ZodString;
|
|
54
|
+
}, z.core.$strip>>;
|
|
55
|
+
markedCount: z.ZodNumber;
|
|
56
|
+
failureCount: z.ZodNumber;
|
|
57
|
+
};
|
|
58
|
+
annotations: {
|
|
59
|
+
readOnlyHint: false;
|
|
60
|
+
destructiveHint: false;
|
|
61
|
+
idempotentHint: true;
|
|
62
|
+
};
|
|
63
|
+
execute(args: {
|
|
64
|
+
workspaceId: number;
|
|
65
|
+
threadIds?: string[] | undefined;
|
|
66
|
+
conversationIds?: string[] | undefined;
|
|
67
|
+
all?: boolean | undefined;
|
|
68
|
+
}, client: CommsApi): Promise<{
|
|
69
|
+
textContent: string;
|
|
70
|
+
structuredContent: MarkReadStructured;
|
|
71
|
+
}>;
|
|
72
|
+
};
|
|
73
|
+
export { markRead, type MarkReadStructured };
|