@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.
Files changed (93) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +198 -34
  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 +85 -8
@@ -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 };