@hardlydifficult/chat 1.1.67 → 1.1.68

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 (2) hide show
  1. package/README.md +249 -331
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hardlydifficult/chat
2
2
 
3
- A unified API for Discord and Slack messaging with rich document support, threading, reactions, and bulk operations.
3
+ A TypeScript library for building chat bots with unified APIs for Discord and Slack, featuring threading, batching, streaming, commands, and cleanup.
4
4
 
5
5
  ## Installation
6
6
 
@@ -10,6 +10,26 @@ npm install @hardlydifficult/chat
10
10
 
11
11
  ## Quick Start
12
12
 
13
+ ```typescript
14
+ import { createChatClient } from '@hardlydifficult/chat';
15
+
16
+ // Create a Discord or Slack client based on platform
17
+ const client = createChatClient('discord', {
18
+ token: process.env.DISCORD_TOKEN!,
19
+ // or: 'slack', { token: process.env.SLACK_TOKEN! }
20
+ });
21
+
22
+ // Listen for messages and respond
23
+ client.onMessage((event) => {
24
+ event.channel.send('Hello, world!');
25
+ });
26
+
27
+ // Start listening
28
+ await client.start();
29
+ ```
30
+
31
+ ### Alternative Quick Start (with Message Operations)
32
+
13
33
  ```typescript
14
34
  import { createChatClient } from "@hardlydifficult/chat";
15
35
 
@@ -21,448 +41,346 @@ const channel = await client.connect("channel-id");
21
41
  await channel.postMessage("Hello world!").addReactions(["👍", "👎"]);
22
42
  ```
23
43
 
24
- ## Core Concepts
44
+ ## Core Abstractions
25
45
 
26
- ### Message Operations
46
+ ### Chat Clients
27
47
 
28
- Messages returned from `postMessage()` support chainable reaction and management operations.
48
+ The library provides platform-specific chat clients through a unified interface.
29
49
 
30
50
  ```typescript
31
- const msg = await channel
32
- .postMessage("Vote: 1, 2, or 3")
33
- .addReactions(["1️⃣", "2️⃣", "3️⃣"])
34
- .onReaction((event) => console.log(`${event.user.username} voted ${event.emoji}`));
35
-
36
- await msg.update("Final count in thread...");
37
- await msg.delete({ cascadeReplies: false });
38
- ```
51
+ import { DiscordChatClient, SlackChatClient, createChatClient } from '@hardlydifficult/chat';
39
52
 
40
- #### Reply Messages
53
+ // Factory function for any supported platform
54
+ const client = createChatClient('discord', { token: '...' });
41
55
 
42
- Replies can be awaited like promises and support reactions before resolution.
43
-
44
- ```typescript
45
- const reply = await msg.reply("Counting votes...");
46
- await reply.update("12 votes for pizza");
47
- await reply.addReactions(["🎉"]);
48
- await reply.waitForReactions();
56
+ // Or instantiate directly
57
+ const discordClient = new DiscordChatClient({ token: '...' });
58
+ const slackClient = new SlackChatClient({ token: '...' });
49
59
  ```
50
60
 
51
- ### Streaming Replies
52
-
53
- Stream text into threads with automatic batching, chunking, and platform limit handling.
61
+ Each client supports:
62
+ - Message event handling via `onMessage()`
63
+ - Reaction event handling via `onReaction()`
64
+ - Platform-specific startup logic via `start()`
65
+ - Graceful shutdown via `stop()`
54
66
 
55
- ```typescript
56
- const stream = thread.stream(1000, abortSignal);
57
- stream.append("Processing...\n");
58
- stream.append("Result: 42\n");
59
- await stream.stop();
60
- ```
67
+ ### Channel and Message
61
68
 
62
- #### Editable Stream
63
-
64
- Updates a single message in-place instead of creating new messages.
69
+ The `Channel` class abstracts messaging operations across platforms.
65
70
 
66
71
  ```typescript
67
- const editableStream = thread.editableStream(2000);
68
- editableStream.append("Step 1...\n");
69
- editableStream.append("Step 2...\n");
70
- await editableStream.stop(); // posts one message, edits it twice
72
+ // Send a message and get a ReplyMessage reference
73
+ const message = await channel.send('Hello');
74
+ await message.react('👍');
75
+ await message.reply('Thanks for the feedback!');
76
+ await message.delete(); // Only the bot's message
77
+ await channel.cleanup('all'); // Delete all bot messages in the channel
71
78
  ```
72
79
 
73
- ### Threads
74
-
75
- Create and manage conversational threads anchored to messages.
80
+ Messages are represented by the `Message` interface:
76
81
 
77
82
  ```typescript
78
- const thread = await channel.createThread("Topic", "Session-1");
79
- await thread.post("How can I help?");
80
- thread.onReply(async (msg) => {
81
- await thread.post(`You said: ${msg.content}`);
82
- });
83
- await thread.delete();
83
+ interface Message {
84
+ id: string;
85
+ author: Member;
86
+ content: string;
87
+ timestamp: Date;
88
+ reactions: Reaction[];
89
+ thread?: Thread;
90
+ channel: Channel;
91
+
92
+ reply(content: string): Promise<ReplyMessage>;
93
+ update(content: string): Promise<void>;
94
+ delete(): Promise<void>;
95
+ react(emoji: string): Promise<void>;
96
+ clearReactions(): Promise<void>;
97
+ }
84
98
  ```
85
99
 
86
- ### Batching Messages
100
+ ### Thread
87
101
 
88
- Group related messages with post-commit operations.
102
+ Threads provide an isolated messaging context with its own lifecycle.
89
103
 
90
104
  ```typescript
91
- const batch = await channel.beginBatch({ key: "report" });
92
- await batch.post("Line 1");
93
- await batch.post("Line 2");
94
- await batch.finish();
95
-
96
- await batch.deleteAll();
97
- await batch.keepLatest(5);
105
+ // Create and use a thread
106
+ const thread = await channel.createThread('Discussions');
107
+ await thread.send('Let us discuss this here.');
108
+ await thread.sendStream('Streaming responses...');
109
+ await thread.cleanup('bot'); // Remove only bot messages
98
110
  ```
99
111
 
100
- #### With Batch Helper
112
+ ## Message Batching
101
113
 
102
- Auto-finish batch even on errors.
114
+ Batching allows grouping multiple messages and managing them collectively.
103
115
 
104
116
  ```typescript
105
- await channel.withBatch(async (batch) => {
106
- await batch.post("First");
107
- await batch.post("Second");
108
- throw new Error("boom"); // batch.finish() called in finally
109
- });
110
- ```
117
+ import { MessageBatch } from '@hardlydifficult/chat';
111
118
 
112
- ### Typing Indicators
119
+ // Create a batch
120
+ const batch = channel.batch();
121
+ batch.add(channel.send('First'));
122
+ batch.add(channel.send('Second'));
113
123
 
114
- Show typing indicators for long-running work.
124
+ // Finish and keep only the latest
125
+ await batch.finish({ mode: 'keepLatest' });
115
126
 
116
- ```typescript
117
- channel.beginTyping();
118
- try {
119
- await longRunningTask();
120
- } finally {
121
- channel.endTyping();
122
- }
123
-
124
- await channel.withTyping(() => processMessages());
127
+ // Or delete everything
128
+ await batch.delete();
125
129
  ```
126
130
 
127
- ### Message Cleanup
128
-
129
- Convenience methods for bulk message management.
131
+ The `MessageBatch` class supports:
132
+ - `add(promise)`: Add a pending message to the batch
133
+ - `finish(options)`: Finalize batch with modes: `'keepAll'`, `'keepLatest'`, `'deleteAll'`
134
+ - `delete()`: Delete all batched messages
135
+ - `query()`: Get list of posted messages
130
136
 
131
- ```typescript
132
- // Keep newest 10, delete rest
133
- await channel.pruneMessages({ keep: 10 });
134
-
135
- // Fetch bot's recent messages
136
- const botMessages = await channel.getRecentBotMessages(50);
137
- ```
138
-
139
- ### Member Matching
137
+ ## Streaming Replies
140
138
 
141
- Resolve users by mention, username, display name, or email.
139
+ The library supports real-time streaming with automatic message updates.
142
140
 
143
141
  ```typescript
144
- await channel.resolveMention("@nick"); // "<@U123>"
145
- await channel.resolveMention("Nick Mancuso"); // "<@U123>"
146
- await channel.resolveMention("nick@example.com"); // "<@U123>"
147
-
148
- const member = await channel.findMember("nick");
142
+ // Use StreamingReply for automatic chunking
143
+ const stream = channel.stream({ mode: 'thread' });
144
+ stream.append('Hello, ');
145
+ await stream.flush(); // Force immediate delivery
146
+ stream.append('world!');
147
+ await stream.finish(); // Finalize with message deletion or edit
149
148
  ```
150
149
 
151
- ### Message Tracker
152
-
153
- Track messages by key for later editing.
150
+ The `EditableStreamReply` class allows in-place editing:
154
151
 
155
152
  ```typescript
156
- const tracker = createMessageTracker((content) => channel.postMessage(content));
157
- tracker.post("status-worker-1", "🔴 Worker disconnected");
158
- // Later:
159
- tracker.edit("status-worker-1", "🟢 Worker reconnected");
153
+ const stream = new EditableStreamReply(thread, { maxMessageLength: 2000 });
154
+ stream.append('Initial ');
155
+ await stream.flush();
156
+ stream.append('content.');
157
+ await stream.finish();
160
158
  ```
161
159
 
162
- ## Command System
160
+ Both support:
161
+ - `append(text)`: Add text to buffer
162
+ - `flush()`: Send current buffer as message update
163
+ - `finish()`: Finalize and cleanup
164
+ - `abort()`: Cancel the stream mid-operation
165
+
166
+ ## Commands
163
167
 
164
- The built-in command framework supports auto-parsed arguments, typing indicators, and message cleanup.
168
+ A regex-based command system with auto-parsing and context-aware routing.
165
169
 
166
170
  ```typescript
167
- import { CommandRegistry, CommandDispatcher, setupJobLifecycle } from "@hardlydifficult/chat";
171
+ import { CommandDispatcher, CommandRegistry, Command } from '@hardlydifficult/chat';
168
172
 
169
173
  const registry = new CommandRegistry();
170
174
 
171
- registry.register("tools", {
172
- prefix: "merge",
173
- description: "Merge pull requests",
174
- args: { type: "rest", argName: "query" },
175
- execute: async (ctx, args) => {
176
- const { thread, abortController } = setupJobLifecycle({
177
- originalMessage: ctx.incomingMessage,
178
- thread: await ctx.startThread("Merge"),
179
- abortController: new AbortController(),
180
- ownerUsername: ctx.incomingMessage.author?.username!,
181
- });
182
-
183
- // Use abortController.signal to support cancellation
184
- const result = await mergePRs(args.query, abortController.signal);
185
- await thread.post(result);
186
- thread.complete();
187
- },
188
- });
189
-
190
- const dispatcher = new CommandDispatcher({
191
- channel,
192
- registry,
193
- state: { inFlightCommands: new Set() },
194
- });
195
- channel.onMessage((msg) => dispatcher.handleMessage(msg));
196
- ```
197
-
198
- ## Platform Config
199
-
200
- ```typescript
201
- // Discord
202
- createChatClient({
203
- type: "discord",
204
- token: process.env.DISCORD_TOKEN,
205
- guildId: process.env.DISCORD_GUILD_ID,
175
+ // Register a command with auto-parsed arguments
176
+ registry.register({
177
+ name: 'ping',
178
+ pattern: /^ping(\s+help)?$/i,
179
+ handler: async ({ context, match }) => {
180
+ if (match[1]) {
181
+ return 'Usage: `ping` or `ping help`';
182
+ }
183
+ return 'Pong!';
184
+ }
206
185
  });
207
186
 
208
- // Slack
209
- createChatClient({
210
- type: "slack",
211
- token: process.env.SLACK_BOT_TOKEN,
212
- appToken: process.env.SLACK_APP_TOKEN,
213
- socketMode: true,
187
+ // Use dispatcher to route messages
188
+ const dispatcher = new CommandDispatcher(registry);
189
+ dispatcher.onMessage((event) => {
190
+ event.channel.typingIndicator.start();
191
+ return dispatcher.handle(event);
214
192
  });
215
193
  ```
216
194
 
217
- ## Document Output
195
+ ### Command Types
218
196
 
219
- Convert structured documents to platform-native rich text.
197
+ Commands are defined using:
220
198
 
221
199
  ```typescript
222
- import { Document, header, text, list, divider, context } from "@hardlydifficult/document-generator";
223
-
224
- const doc = new Document()
225
- .add(header("Status Report"))
226
- .add(divider())
227
- .add(text("All systems operational."))
228
- .add(list(["API: ✅", "DB: ✅", "Cache: ✅"]))
229
- .add(context("Generated at " + new Date().toISOString()));
230
-
231
- await channel.postMessage(doc);
200
+ interface Command {
201
+ name: string;
202
+ pattern: RegExp;
203
+ handler: (args: {
204
+ context: Context;
205
+ match: RegExpExecArray;
206
+ event: MessageEvent;
207
+ }) => Promise<string | void>;
208
+ }
232
209
  ```
233
210
 
234
- ## Typing
235
-
236
- All core types are exported for direct use.
211
+ Contexts include channel, user, and optional state:
237
212
 
238
213
  ```typescript
239
- import type { Member, Message, Thread, MessageBatch } from "@hardlydifficult/chat";
214
+ interface Context {
215
+ channel: Channel;
216
+ user: Member;
217
+ state?: Map<string, any>;
218
+ }
240
219
  ```
241
220
 
242
- ## Appendix
243
-
244
- ### Platform Differences
245
-
246
- | Feature | Discord | Slack |
247
- |------------------------|-----------------------------------|-----------------------------------|
248
- | Typing indicators | ✅ Supported | ❌ No API support (no-op) |
249
- | Message length limit | 2000 characters | 4000 characters |
250
- | Thread creation | Explicit thread channel | Implicit via parent message ts |
251
- | Bulk delete | ✅ Up to 100 messages at once | ❌ Must delete one-by-one |
252
- | Emoji format | Plain Unicode or `:name:` | Colon-wrapped `:name:` |
253
- | File uploads | As attachments | Via `filesUploadV2` API |
254
-
255
- ### Additional Features from Current README
221
+ ### Job Lifecycle
256
222
 
257
- #### Bot Identity
258
-
259
- After `connect()`, `client.me` exposes the authenticated bot user:
223
+ Long-running commands support cancel/dismiss UI flows:
260
224
 
261
225
  ```typescript
262
- const client = createChatClient({ type: "slack" });
263
- await client.connect(channelId);
226
+ import { startJobLifecycle } from '@hardlydifficult/chat';
264
227
 
265
- console.log(client.me?.id); // "U09B00R2R96"
266
- console.log(client.me?.username); // "sprint-bot"
267
- console.log(client.me?.mention); // "<@U09B00R2R96>"
228
+ await startJobLifecycle(thread, {
229
+ onCancel: async () => {
230
+ // Handle user-initiated cancellation
231
+ },
232
+ onDismiss: async () => {
233
+ // Add dismiss emoji for user to remove later
234
+ }
235
+ });
268
236
  ```
269
237
 
270
- #### Incoming Messages
238
+ ## Member Matching
271
239
 
272
- Subscribe to new messages in a channel. The callback receives a full `Message` object — you can delete it, react to it, or reply in its thread.
240
+ Resolve users by mention, ID, or fuzzy match:
273
241
 
274
242
  ```typescript
275
- const unsubscribe = channel.onMessage((msg) => {
276
- console.log(`${msg.author.username}: ${msg.content}`);
243
+ import { matchMember } from '@hardlydifficult/chat';
277
244
 
278
- // Delete the user's command message
279
- msg.delete();
280
-
281
- // React to it
282
- msg.addReactions(["white_check_mark"]);
283
-
284
- // Reply in the message's thread
285
- msg.reply("Got it!");
286
- });
245
+ // Match a member by mention or name
246
+ const member = await matchMember(
247
+ channel,
248
+ '@john', // or 'john', or '123456789'
249
+ { fuzzyThreshold: 0.7 }
250
+ );
287
251
 
288
- // Later: stop listening
289
- unsubscribe();
252
+ // Use aliases for more lenient matching
253
+ const aliases = { 'johnny': 'john' };
254
+ const member = await matchMember(channel, 'johnny', { aliases });
290
255
  ```
291
256
 
292
- Messages from the bot itself are automatically filtered out.
293
-
294
- #### Oversized Message Handling
295
-
296
- Messages that exceed platform limits (Discord: 2000 chars, Slack: 4000 chars) are handled automatically:
297
-
298
- - **`postMessage`**: Sends the full content as a `message.txt` file attachment instead of failing
299
- - **`update`**: Truncates with `…` (edits cannot attach files on either platform)
257
+ ## Output Formatting
300
258
 
301
- No caller changes needed the library handles this transparently.
302
-
303
- #### File Attachments
304
-
305
- Send files as message attachments.
259
+ Convert abstract document blocks into platform-specific formats.
306
260
 
307
261
  ```typescript
308
- channel.postMessage("Here's the scan report", {
309
- files: [
310
- { content: Buffer.from(markdownContent), name: "report.md" },
311
- { content: "plain text content", name: "notes.txt" },
312
- ],
313
- });
262
+ import { formatDiscord, formatSlack } from '@hardlydifficult/chat';
263
+
264
+ const document = {
265
+ type: 'doc',
266
+ content: [
267
+ { type: 'text', text: 'Hello ' },
268
+ { type: 'bold', content: 'world' }
269
+ ]
270
+ };
271
+
272
+ const discordPayload = formatDiscord(document);
273
+ const slackBlocks = formatSlack(document);
314
274
  ```
315
275
 
316
- #### Dismissable Messages
317
-
318
- Post a message that the specified user can dismiss by clicking the trash reaction.
319
-
320
- ```typescript
321
- await channel.postDismissable("Build complete!", user.id);
322
- ```
276
+ Supported block types include:
277
+ - `text`, `bold`, `italic`, `code`, `pre`
278
+ - `header`, `divider`, `section`, `button`
279
+ - `mention` (platform-specific user/channel reference)
323
280
 
324
- #### Declarative Reactions
281
+ ## Message Tracking
325
282
 
326
- `setReactions` manages the full reaction state on a message. It diffs against the previous `setReactions` call, removing stale emojis and adding new ones, and replaces any existing reaction handler.
283
+ Track and update messages by key for efficient dynamic updates.
327
284
 
328
285
  ```typescript
329
- const msg = await channel.postMessage("PR #42: open");
286
+ import { MessageTracker } from '@hardlydifficult/chat';
330
287
 
331
- // Set initial reactions
332
- msg.setReactions(["🟡"], (event) => handlePending(event));
333
-
334
- // Later: update to merged state — removes 🟡, adds 🟢, swaps handler
335
- msg.setReactions(["🟢"], (event) => handleMerged(event));
288
+ const tracker = new MessageTracker(channel);
289
+ tracker.track('status', async () => channel.send('Updating...'));
290
+ tracker.edit('status', async (message) => {
291
+ await message.update('Updated!');
292
+ });
336
293
  ```
337
294
 
338
- #### Message Batches
295
+ ## File Operations
339
296
 
340
- Group related posted messages so they can be retrieved and cleaned up together.
297
+ Post and update messages with file attachments.
341
298
 
342
299
  ```typescript
343
- const batch = await channel.beginBatch({ key: "sprint-update" });
344
-
345
- for (const member of members) {
346
- const msg = await batch.post(summary(member));
347
- await msg.reply(detail(member));
348
- }
300
+ // Discord: attach files via buildMessagePayload
301
+ import { buildMessagePayload } from '@hardlydifficult/chat/discord';
349
302
 
350
- await batch.post(callouts);
351
- await batch.finish();
352
-
353
- const recent = await channel.getBatches({
354
- key: "sprint-update",
355
- author: "me",
356
- limit: 5,
303
+ const payload = buildMessagePayload({
304
+ content: 'Report',
305
+ files: [{ filename: 'report.pdf', data: buffer }]
357
306
  });
358
307
 
359
- await recent[0].deleteAll({ cascadeReplies: true });
360
- ```
361
-
362
- For safer lifecycle handling, use `withBatch` (auto-finishes in `finally`):
308
+ await channel.send(payload);
363
309
 
364
- ```typescript
365
- await channel.withBatch({ key: "sprint-update" }, async (batch) => {
366
- await batch.post("Part 1");
367
- await batch.post("Part 2");
310
+ // Slack: use built-in file support
311
+ await slackClient.postMessage({
312
+ channel: 'C123',
313
+ text: 'Report',
314
+ file: buffer,
315
+ filename: 'report.pdf'
368
316
  });
369
317
  ```
370
318
 
371
- #### Threads (Enhanced)
372
-
373
- You can also create a thread from an existing message:
374
-
375
- ```typescript
376
- const msg = await channel.postMessage("Starting a discussion");
377
- const thread = await msg.startThread("Discussion Thread", 1440); // auto-archive in minutes
378
- ```
379
-
380
- Reconnect to an existing thread by ID (e.g., after a restart):
381
-
382
- ```typescript
383
- const thread = channel.openThread(savedThreadId);
384
- await thread.post("I'm back!");
385
- thread.onReply(async (msg) => { /* ... */ });
386
- ```
319
+ ## Setup
387
320
 
388
- #### Streaming Replies (Enhanced)
321
+ ### Discord
389
322
 
390
- Both `streamReply()`, `thread.stream()`, and `thread.editableStream()` accept an optional `AbortSignal` to automatically stop the stream on cancellation. After abort, `append()` becomes a no-op and `stop()` is called automatically.
323
+ 1. Create a bot at [Discord Developer Portal](https://discord.com/developers/applications)
324
+ 2. Enable **MESSAGE CONTENT INTENT** in bot settings
325
+ 3. Invite with `bot` and `applications.commands` scopes
326
+ 4. Set `DISCORD_TOKEN` environment variable
391
327
 
392
- ```typescript
393
- const controller = new AbortController();
394
- const stream = thread.stream(2000, controller.signal);
328
+ ### Slack
395
329
 
396
- stream.append("working...\n");
397
- controller.abort(); // auto-stops, future appends are ignored
398
- console.log(stream.content); // "working...\n" only pre-abort text
399
- ```
330
+ 1. Create a Slack app at [Slack API](https://api.slack.com/apps)
331
+ 2. Add **Incoming Webhooks** and **Bot User Token** scopes
332
+ 3. Install to workspace and copy token
333
+ 4. Set `SLACK_TOKEN` environment variable
400
334
 
401
- #### Typing Indicator (Enhanced)
335
+ ## Platform Differences
402
336
 
403
- For one-shot use, `sendTyping()` sends a single indicator without automatic refresh:
337
+ | Feature | Discord | Slack |
338
+ |----------------------------|------------------------------------|------------------------------------|
339
+ | Message Length Limit | 2000 characters | 4000 characters |
340
+ | Thread Support | Text channel threads | Conversations with replies |
341
+ | File Uploads | Via `buildMessagePayload` | Via `chat.postMessage` |
342
+ | Typing Indicators | Supported via `typingIndicator` | Supported via `chat.scheduledMsg` |
343
+ | Message History | Requires **MESSAGE CONTENT INTENT**| Requires `history` scope |
344
+ | Mention Resolution | By ID or username#discriminator | By `@username` or user ID |
345
+ | Emoji Reactions | Unicode or custom emoji (with ID) | Custom emoji only by name |
404
346
 
405
- ```typescript
406
- await channel.sendTyping();
407
- ```
347
+ ## Appendix
408
348
 
409
- > **Slack note:** Slack does not support bot typing indicators. Both methods are no-ops on Slack.
349
+ ### Error Handling
410
350
 
411
- #### Bulk Operations (Enhanced)
351
+ Platform-specific error codes are mapped to user-friendly messages:
412
352
 
413
353
  ```typescript
414
- // Delete up to 100 recent messages
415
- const deletedCount = await channel.bulkDelete(50);
416
-
417
- // List and filter recent messages
418
- const botMessages = await channel.getMessages({ limit: 50, author: "me" });
419
- const sameMessages = await channel.getRecentBotMessages(50);
420
-
421
- // Keep latest 8 bot messages, delete older ones (opinionated cleanup helper)
422
- await channel.pruneMessages({ author: "me", limit: 50, keep: 8 });
354
+ import { isRecoverableError, getErrorFriendlyMessage } from '@hardlydifficult/chat';
423
355
 
424
- // Get all threads (active and archived) and delete them
425
- const threads = await channel.getThreads();
426
- for (const thread of threads) {
427
- await thread.delete();
356
+ try {
357
+ await channel.send('Failed message');
358
+ } catch (error) {
359
+ if (isRecoverableError(error)) {
360
+ // Retry with backoff
361
+ } else {
362
+ console.log(getErrorFriendlyMessage(error));
363
+ }
428
364
  }
429
365
  ```
430
366
 
431
- > **Slack note:** Slack has no bulk delete API — messages are deleted one-by-one. Some may fail if the bot lacks permission to delete others' messages. `getThreads()` scans recent channel history for messages with replies.
432
-
433
- #### Connection Resilience
434
-
435
- Both platforms auto-reconnect via their underlying libraries (discord.js and @slack/bolt). Register callbacks for observability.
436
-
437
- ```typescript
438
- const client = createChatClient({ type: "discord" });
439
-
440
- client.onDisconnect((reason) => {
441
- console.log("Disconnected:", reason);
442
- });
443
-
444
- client.onError((error) => {
445
- console.error("Connection error:", error);
446
- });
447
-
448
- await client.disconnect(); // clean shutdown
449
- ```
450
-
451
- Both callbacks return an unsubscribe function.
367
+ Recoverable errors include:
368
+ - Rate limits (`429 Too Many Requests`)
369
+ - Network timeouts
370
+ - Temporary unavailability
452
371
 
453
- ## Platform Setup
372
+ ### Cleanup Modes
454
373
 
455
- ### Discord
374
+ The `cleanup` method supports:
456
375
 
457
- 1. Create bot at [Discord Developer Portal](https://discord.com/developers/applications)
458
- 2. Enable Gateway Intents: `GUILDS`, `GUILD_MEMBERS`, `GUILD_MESSAGES`, `GUILD_MESSAGE_REACTIONS`, `MESSAGE_CONTENT`
459
- 3. Bot permissions: `Send Messages`, `Add Reactions`, `Read Message History`, `Manage Messages` (for bulk delete), `Create Public Threads`, `Send Messages in Threads`
460
- 4. Set `DISCORD_TOKEN` and `DISCORD_GUILD_ID` env vars
376
+ | Mode | Behavior |
377
+ |-----------|----------------------------------------------|
378
+ | `all` | Delete all messages (bot + users) |
379
+ | `bot` | Delete only bot-authored messages |
380
+ | `user` | Delete only user-authored messages |
381
+ | `since` | Delete messages newer than timestamp |
461
382
 
462
- ### Slack
463
-
464
- 1. Create app at [Slack API](https://api.slack.com/apps)
465
- 2. Enable Socket Mode, generate App Token
466
- 3. Bot scopes: `chat:write`, `chat:write.public`, `reactions:write`, `reactions:read`, `channels:history`, `channels:read`, `files:write`, `users:read`
467
- 4. Subscribe to events: `reaction_added`, `message.channels`
468
- 5. Set `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` env vars
383
+ Example:
384
+ ```typescript
385
+ await channel.cleanup({ mode: 'since', timestamp: new Date(Date.now() - 86400000) });
386
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardlydifficult/chat",
3
- "version": "1.1.67",
3
+ "version": "1.1.68",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [