@herdctl/discord 0.0.1

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 (137) hide show
  1. package/dist/__tests__/auto-mode-handler.test.d.ts +2 -0
  2. package/dist/__tests__/auto-mode-handler.test.d.ts.map +1 -0
  3. package/dist/__tests__/auto-mode-handler.test.js +362 -0
  4. package/dist/__tests__/auto-mode-handler.test.js.map +1 -0
  5. package/dist/__tests__/discord-connector.test.d.ts +2 -0
  6. package/dist/__tests__/discord-connector.test.d.ts.map +1 -0
  7. package/dist/__tests__/discord-connector.test.js +958 -0
  8. package/dist/__tests__/discord-connector.test.js.map +1 -0
  9. package/dist/__tests__/error-handler.test.d.ts +2 -0
  10. package/dist/__tests__/error-handler.test.d.ts.map +1 -0
  11. package/dist/__tests__/error-handler.test.js +509 -0
  12. package/dist/__tests__/error-handler.test.js.map +1 -0
  13. package/dist/__tests__/errors.test.d.ts +2 -0
  14. package/dist/__tests__/errors.test.d.ts.map +1 -0
  15. package/dist/__tests__/errors.test.js +152 -0
  16. package/dist/__tests__/errors.test.js.map +1 -0
  17. package/dist/__tests__/logger.test.d.ts +2 -0
  18. package/dist/__tests__/logger.test.d.ts.map +1 -0
  19. package/dist/__tests__/logger.test.js +282 -0
  20. package/dist/__tests__/logger.test.js.map +1 -0
  21. package/dist/__tests__/mention-handler.test.d.ts +2 -0
  22. package/dist/__tests__/mention-handler.test.d.ts.map +1 -0
  23. package/dist/__tests__/mention-handler.test.js +547 -0
  24. package/dist/__tests__/mention-handler.test.js.map +1 -0
  25. package/dist/auto-mode-handler.d.ts +145 -0
  26. package/dist/auto-mode-handler.d.ts.map +1 -0
  27. package/dist/auto-mode-handler.js +211 -0
  28. package/dist/auto-mode-handler.js.map +1 -0
  29. package/dist/commands/__tests__/command-manager.test.d.ts +2 -0
  30. package/dist/commands/__tests__/command-manager.test.d.ts.map +1 -0
  31. package/dist/commands/__tests__/command-manager.test.js +307 -0
  32. package/dist/commands/__tests__/command-manager.test.js.map +1 -0
  33. package/dist/commands/__tests__/help.test.d.ts +2 -0
  34. package/dist/commands/__tests__/help.test.d.ts.map +1 -0
  35. package/dist/commands/__tests__/help.test.js +105 -0
  36. package/dist/commands/__tests__/help.test.js.map +1 -0
  37. package/dist/commands/__tests__/reset.test.d.ts +2 -0
  38. package/dist/commands/__tests__/reset.test.d.ts.map +1 -0
  39. package/dist/commands/__tests__/reset.test.js +140 -0
  40. package/dist/commands/__tests__/reset.test.js.map +1 -0
  41. package/dist/commands/__tests__/status.test.d.ts +2 -0
  42. package/dist/commands/__tests__/status.test.d.ts.map +1 -0
  43. package/dist/commands/__tests__/status.test.js +205 -0
  44. package/dist/commands/__tests__/status.test.js.map +1 -0
  45. package/dist/commands/command-manager.d.ts +66 -0
  46. package/dist/commands/command-manager.d.ts.map +1 -0
  47. package/dist/commands/command-manager.js +191 -0
  48. package/dist/commands/command-manager.js.map +1 -0
  49. package/dist/commands/help.d.ts +8 -0
  50. package/dist/commands/help.d.ts.map +1 -0
  51. package/dist/commands/help.js +27 -0
  52. package/dist/commands/help.js.map +1 -0
  53. package/dist/commands/index.d.ts +12 -0
  54. package/dist/commands/index.d.ts.map +1 -0
  55. package/dist/commands/index.js +13 -0
  56. package/dist/commands/index.js.map +1 -0
  57. package/dist/commands/reset.d.ts +9 -0
  58. package/dist/commands/reset.d.ts.map +1 -0
  59. package/dist/commands/reset.js +28 -0
  60. package/dist/commands/reset.js.map +1 -0
  61. package/dist/commands/status.d.ts +9 -0
  62. package/dist/commands/status.d.ts.map +1 -0
  63. package/dist/commands/status.js +102 -0
  64. package/dist/commands/status.js.map +1 -0
  65. package/dist/commands/types.d.ts +87 -0
  66. package/dist/commands/types.d.ts.map +1 -0
  67. package/dist/commands/types.js +8 -0
  68. package/dist/commands/types.js.map +1 -0
  69. package/dist/discord-connector.d.ts +154 -0
  70. package/dist/discord-connector.d.ts.map +1 -0
  71. package/dist/discord-connector.js +638 -0
  72. package/dist/discord-connector.js.map +1 -0
  73. package/dist/error-handler.d.ts +237 -0
  74. package/dist/error-handler.d.ts.map +1 -0
  75. package/dist/error-handler.js +433 -0
  76. package/dist/error-handler.js.map +1 -0
  77. package/dist/errors.d.ts +61 -0
  78. package/dist/errors.d.ts.map +1 -0
  79. package/dist/errors.js +77 -0
  80. package/dist/errors.js.map +1 -0
  81. package/dist/index.d.ts +34 -0
  82. package/dist/index.d.ts.map +1 -0
  83. package/dist/index.js +36 -0
  84. package/dist/index.js.map +1 -0
  85. package/dist/logger.d.ts +119 -0
  86. package/dist/logger.d.ts.map +1 -0
  87. package/dist/logger.js +198 -0
  88. package/dist/logger.js.map +1 -0
  89. package/dist/mention-handler.d.ts +176 -0
  90. package/dist/mention-handler.d.ts.map +1 -0
  91. package/dist/mention-handler.js +236 -0
  92. package/dist/mention-handler.js.map +1 -0
  93. package/dist/session-manager/__tests__/errors.test.d.ts +2 -0
  94. package/dist/session-manager/__tests__/errors.test.d.ts.map +1 -0
  95. package/dist/session-manager/__tests__/errors.test.js +124 -0
  96. package/dist/session-manager/__tests__/errors.test.js.map +1 -0
  97. package/dist/session-manager/__tests__/session-manager.test.d.ts +2 -0
  98. package/dist/session-manager/__tests__/session-manager.test.d.ts.map +1 -0
  99. package/dist/session-manager/__tests__/session-manager.test.js +517 -0
  100. package/dist/session-manager/__tests__/session-manager.test.js.map +1 -0
  101. package/dist/session-manager/__tests__/types.test.d.ts +2 -0
  102. package/dist/session-manager/__tests__/types.test.d.ts.map +1 -0
  103. package/dist/session-manager/__tests__/types.test.js +169 -0
  104. package/dist/session-manager/__tests__/types.test.js.map +1 -0
  105. package/dist/session-manager/errors.d.ts +58 -0
  106. package/dist/session-manager/errors.d.ts.map +1 -0
  107. package/dist/session-manager/errors.js +70 -0
  108. package/dist/session-manager/errors.js.map +1 -0
  109. package/dist/session-manager/index.d.ts +11 -0
  110. package/dist/session-manager/index.d.ts.map +1 -0
  111. package/dist/session-manager/index.js +12 -0
  112. package/dist/session-manager/index.js.map +1 -0
  113. package/dist/session-manager/session-manager.d.ts +107 -0
  114. package/dist/session-manager/session-manager.d.ts.map +1 -0
  115. package/dist/session-manager/session-manager.js +347 -0
  116. package/dist/session-manager/session-manager.js.map +1 -0
  117. package/dist/session-manager/types.d.ts +167 -0
  118. package/dist/session-manager/types.d.ts.map +1 -0
  119. package/dist/session-manager/types.js +57 -0
  120. package/dist/session-manager/types.js.map +1 -0
  121. package/dist/types.d.ts +323 -0
  122. package/dist/types.d.ts.map +1 -0
  123. package/dist/types.js +8 -0
  124. package/dist/types.js.map +1 -0
  125. package/dist/utils/__tests__/formatting.test.d.ts +2 -0
  126. package/dist/utils/__tests__/formatting.test.d.ts.map +1 -0
  127. package/dist/utils/__tests__/formatting.test.js +571 -0
  128. package/dist/utils/__tests__/formatting.test.js.map +1 -0
  129. package/dist/utils/formatting.d.ts +211 -0
  130. package/dist/utils/formatting.d.ts.map +1 -0
  131. package/dist/utils/formatting.js +305 -0
  132. package/dist/utils/formatting.js.map +1 -0
  133. package/dist/utils/index.d.ts +5 -0
  134. package/dist/utils/index.d.ts.map +1 -0
  135. package/dist/utils/index.js +9 -0
  136. package/dist/utils/index.js.map +1 -0
  137. package/package.json +49 -0
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Message formatting utilities for Discord
3
+ *
4
+ * Provides utilities for:
5
+ * - Splitting long messages to fit Discord's 2000 character limit
6
+ * - Maintaining message coherence when splitting (avoiding mid-sentence breaks)
7
+ * - Managing typing indicators during message processing
8
+ */
9
+ import type { TextChannel, DMChannel, NewsChannel, ThreadChannel } from "discord.js";
10
+ /**
11
+ * Discord's maximum message length
12
+ */
13
+ export declare const DISCORD_MAX_MESSAGE_LENGTH = 2000;
14
+ /**
15
+ * Default delay between sending split messages (in milliseconds)
16
+ */
17
+ export declare const DEFAULT_MESSAGE_DELAY_MS = 500;
18
+ /**
19
+ * Minimum chunk size when splitting messages
20
+ * Prevents creating very small message fragments
21
+ */
22
+ export declare const MIN_CHUNK_SIZE = 100;
23
+ /**
24
+ * Supported text-based channel types that can receive messages
25
+ */
26
+ export type SendableChannel = TextChannel | DMChannel | NewsChannel | ThreadChannel;
27
+ /**
28
+ * Options for splitting messages
29
+ */
30
+ export interface MessageSplitOptions {
31
+ /**
32
+ * Maximum length for each message chunk (default: 2000)
33
+ */
34
+ maxLength?: number;
35
+ /**
36
+ * Whether to try to split at natural boundaries like sentences (default: true)
37
+ */
38
+ preserveBoundaries?: boolean;
39
+ /**
40
+ * Characters to use as split points, in order of preference (default: ['\n\n', '\n', '. ', '! ', '? ', ', ', ' '])
41
+ */
42
+ splitPoints?: string[];
43
+ }
44
+ /**
45
+ * Options for sending split messages
46
+ */
47
+ export interface SendSplitOptions extends MessageSplitOptions {
48
+ /**
49
+ * Delay between messages in milliseconds (default: 500)
50
+ */
51
+ delayMs?: number;
52
+ }
53
+ /**
54
+ * Result from splitting a message
55
+ */
56
+ export interface SplitResult {
57
+ /**
58
+ * Array of message chunks
59
+ */
60
+ chunks: string[];
61
+ /**
62
+ * Whether the message was split
63
+ */
64
+ wasSplit: boolean;
65
+ /**
66
+ * Original message length
67
+ */
68
+ originalLength: number;
69
+ }
70
+ /**
71
+ * Find the best split point within a text chunk
72
+ *
73
+ * @param text - Text to find split point in
74
+ * @param maxLength - Maximum length for the chunk
75
+ * @param splitPoints - Split points to search for, in order of preference
76
+ * @returns Index to split at, or maxLength if no good split point found
77
+ */
78
+ export declare function findSplitPoint(text: string, maxLength: number, splitPoints?: string[]): number;
79
+ /**
80
+ * Split a message into chunks that fit Discord's message length limit
81
+ *
82
+ * @param content - Message content to split
83
+ * @param options - Split options
84
+ * @returns Split result with chunks array
85
+ *
86
+ * @example
87
+ * ```typescript
88
+ * const result = splitMessage(longText);
89
+ * for (const chunk of result.chunks) {
90
+ * await channel.send(chunk);
91
+ * }
92
+ * ```
93
+ */
94
+ export declare function splitMessage(content: string, options?: MessageSplitOptions): SplitResult;
95
+ /**
96
+ * Check if a message needs to be split
97
+ *
98
+ * @param content - Message content to check
99
+ * @param maxLength - Maximum message length (default: 2000)
100
+ * @returns true if the message exceeds the max length
101
+ */
102
+ export declare function needsSplit(content: string, maxLength?: number): boolean;
103
+ /**
104
+ * Controller for managing a typing indicator loop
105
+ */
106
+ export interface TypingController {
107
+ /**
108
+ * Stop the typing indicator
109
+ */
110
+ stop(): void;
111
+ /**
112
+ * Whether the typing indicator is currently active
113
+ */
114
+ readonly isActive: boolean;
115
+ }
116
+ /**
117
+ * Start a typing indicator that refreshes automatically
118
+ *
119
+ * Discord typing indicators expire after ~10 seconds, so this function
120
+ * sets up an interval to keep refreshing the typing status until stopped.
121
+ *
122
+ * @param channel - Channel to show typing indicator in
123
+ * @param refreshInterval - How often to refresh (default: 5000ms)
124
+ * @returns Controller to stop the typing indicator
125
+ *
126
+ * @example
127
+ * ```typescript
128
+ * const typing = startTypingIndicator(channel);
129
+ * try {
130
+ * const response = await processMessage(prompt);
131
+ * await channel.send(response);
132
+ * } finally {
133
+ * typing.stop();
134
+ * }
135
+ * ```
136
+ */
137
+ export declare function startTypingIndicator(channel: SendableChannel, refreshInterval?: number): TypingController;
138
+ /**
139
+ * Send a message, automatically splitting if needed
140
+ *
141
+ * @param channel - Channel to send the message to
142
+ * @param content - Message content to send
143
+ * @param options - Send options including split and delay settings
144
+ * @returns Array of message IDs for sent messages
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * const messageIds = await sendSplitMessage(channel, longResponse, {
149
+ * delayMs: 500,
150
+ * preserveBoundaries: true,
151
+ * });
152
+ * console.log(`Sent ${messageIds.length} messages`);
153
+ * ```
154
+ */
155
+ export declare function sendSplitMessage(channel: SendableChannel, content: string, options?: SendSplitOptions): Promise<string[]>;
156
+ /**
157
+ * Send a message with typing indicator
158
+ *
159
+ * Shows a typing indicator, processes the content, then sends the response.
160
+ * Automatically splits long messages.
161
+ *
162
+ * @param channel - Channel to send the message to
163
+ * @param contentProvider - Async function that generates the message content
164
+ * @param options - Send options
165
+ * @returns Array of message IDs for sent messages
166
+ *
167
+ * @example
168
+ * ```typescript
169
+ * const messageIds = await sendWithTyping(channel, async () => {
170
+ * return await generateResponse(prompt);
171
+ * });
172
+ * ```
173
+ */
174
+ export declare function sendWithTyping(channel: SendableChannel, contentProvider: () => Promise<string>, options?: SendSplitOptions): Promise<string[]>;
175
+ /**
176
+ * Truncate a message to fit within the max length, adding an ellipsis
177
+ *
178
+ * @param content - Message content to truncate
179
+ * @param maxLength - Maximum length (default: 2000)
180
+ * @param ellipsis - Ellipsis to append (default: '...')
181
+ * @returns Truncated message
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * const short = truncateMessage(longText, 100);
186
+ * // Returns: "This is a very long text that has been trun..."
187
+ * ```
188
+ */
189
+ export declare function truncateMessage(content: string, maxLength?: number, ellipsis?: string): string;
190
+ /**
191
+ * Format code as a Discord code block
192
+ *
193
+ * @param code - Code to format
194
+ * @param language - Optional language for syntax highlighting
195
+ * @returns Formatted code block
196
+ *
197
+ * @example
198
+ * ```typescript
199
+ * const formatted = formatCodeBlock('const x = 1;', 'typescript');
200
+ * // Returns: "```typescript\nconst x = 1;\n```"
201
+ * ```
202
+ */
203
+ export declare function formatCodeBlock(code: string, language?: string): string;
204
+ /**
205
+ * Escape Discord markdown characters in text
206
+ *
207
+ * @param text - Text to escape
208
+ * @returns Text with markdown characters escaped
209
+ */
210
+ export declare function escapeMarkdown(text: string): string;
211
+ //# sourceMappingURL=formatting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatting.d.ts","sourceRoot":"","sources":["../../src/utils/formatting.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,WAAW,EACX,SAAS,EACT,WAAW,EACX,aAAa,EACd,MAAM,YAAY,CAAC;AAMpB;;GAEG;AACH,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C;;GAEG;AACH,eAAO,MAAM,wBAAwB,MAAM,CAAC;AAE5C;;;GAGG;AACH,eAAO,MAAM,cAAc,MAAM,CAAC;AAMlC;;GAEG;AACH,MAAM,MAAM,eAAe,GACvB,WAAW,GACX,SAAS,GACT,WAAW,GACX,aAAa,CAAC;AAElB;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAE7B;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAiB,SAAQ,mBAAmB;IAC3D;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B;;OAEG;IACH,MAAM,EAAE,MAAM,EAAE,CAAC;IAEjB;;OAEG;IACH,QAAQ,EAAE,OAAO,CAAC;IAElB;;OAEG;IACH,cAAc,EAAE,MAAM,CAAC;CACxB;AAaD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,WAAW,GAAE,MAAM,EAAyB,GAC3C,MAAM,CA4BR;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,YAAY,CAC1B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,mBAAwB,GAChC,WAAW,CAoDb;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,MAAM,EACf,SAAS,GAAE,MAAmC,GAC7C,OAAO,CAET;AAMD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,IAAI,IAAI,IAAI,CAAC;IAEb;;OAEG;IACH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,eAAe,EACxB,eAAe,GAAE,MAAa,GAC7B,gBAAgB,CA8BlB;AAMD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,MAAM,EAAE,CAAC,CAoBnB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,eAAe,EACxB,eAAe,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EACtC,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,MAAM,EAAE,CAAC,CASnB;AAeD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,SAAS,GAAE,MAAmC,EAC9C,QAAQ,GAAE,MAAc,GACvB,MAAM,CAOR;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAGvE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEnD"}
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Message formatting utilities for Discord
3
+ *
4
+ * Provides utilities for:
5
+ * - Splitting long messages to fit Discord's 2000 character limit
6
+ * - Maintaining message coherence when splitting (avoiding mid-sentence breaks)
7
+ * - Managing typing indicators during message processing
8
+ */
9
+ // =============================================================================
10
+ // Constants
11
+ // =============================================================================
12
+ /**
13
+ * Discord's maximum message length
14
+ */
15
+ export const DISCORD_MAX_MESSAGE_LENGTH = 2000;
16
+ /**
17
+ * Default delay between sending split messages (in milliseconds)
18
+ */
19
+ export const DEFAULT_MESSAGE_DELAY_MS = 500;
20
+ /**
21
+ * Minimum chunk size when splitting messages
22
+ * Prevents creating very small message fragments
23
+ */
24
+ export const MIN_CHUNK_SIZE = 100;
25
+ // =============================================================================
26
+ // Message Splitting
27
+ // =============================================================================
28
+ /**
29
+ * Default split points in order of preference
30
+ *
31
+ * We prefer to split at paragraph breaks, then sentences, then clauses, then words
32
+ */
33
+ const DEFAULT_SPLIT_POINTS = ["\n\n", "\n", ". ", "! ", "? ", ", ", " "];
34
+ /**
35
+ * Find the best split point within a text chunk
36
+ *
37
+ * @param text - Text to find split point in
38
+ * @param maxLength - Maximum length for the chunk
39
+ * @param splitPoints - Split points to search for, in order of preference
40
+ * @returns Index to split at, or maxLength if no good split point found
41
+ */
42
+ export function findSplitPoint(text, maxLength, splitPoints = DEFAULT_SPLIT_POINTS) {
43
+ // If text fits, no split needed
44
+ if (text.length <= maxLength) {
45
+ return text.length;
46
+ }
47
+ // Try each split point in order of preference
48
+ for (const splitPoint of splitPoints) {
49
+ // Search backwards from maxLength to find the last occurrence of this split point
50
+ const searchText = text.slice(0, maxLength);
51
+ const lastIndex = searchText.lastIndexOf(splitPoint);
52
+ // If found and results in a reasonable chunk size
53
+ if (lastIndex > MIN_CHUNK_SIZE) {
54
+ // Include the split point in the first chunk (e.g., keep the period with the sentence)
55
+ return lastIndex + splitPoint.length;
56
+ }
57
+ }
58
+ // No good split point found - fall back to hard split at maxLength
59
+ // But try to avoid splitting in the middle of a word
60
+ const hardSplitIndex = text.lastIndexOf(" ", maxLength);
61
+ if (hardSplitIndex > MIN_CHUNK_SIZE) {
62
+ return hardSplitIndex + 1; // Include the space in the first chunk
63
+ }
64
+ // Last resort: hard split at maxLength
65
+ return maxLength;
66
+ }
67
+ /**
68
+ * Split a message into chunks that fit Discord's message length limit
69
+ *
70
+ * @param content - Message content to split
71
+ * @param options - Split options
72
+ * @returns Split result with chunks array
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const result = splitMessage(longText);
77
+ * for (const chunk of result.chunks) {
78
+ * await channel.send(chunk);
79
+ * }
80
+ * ```
81
+ */
82
+ export function splitMessage(content, options = {}) {
83
+ const { maxLength = DISCORD_MAX_MESSAGE_LENGTH, preserveBoundaries = true, splitPoints = DEFAULT_SPLIT_POINTS, } = options;
84
+ const originalLength = content.length;
85
+ // If content fits in one message, return as-is
86
+ if (content.length <= maxLength) {
87
+ return {
88
+ chunks: [content],
89
+ wasSplit: false,
90
+ originalLength,
91
+ };
92
+ }
93
+ const chunks = [];
94
+ let remaining = content;
95
+ while (remaining.length > 0) {
96
+ if (remaining.length <= maxLength) {
97
+ // Remaining text fits in one message
98
+ chunks.push(remaining.trim());
99
+ break;
100
+ }
101
+ // Find the best split point
102
+ let splitIndex;
103
+ if (preserveBoundaries) {
104
+ splitIndex = findSplitPoint(remaining, maxLength, splitPoints);
105
+ }
106
+ else {
107
+ // Simple split at maxLength
108
+ splitIndex = maxLength;
109
+ }
110
+ // Extract the chunk and trim
111
+ const chunk = remaining.slice(0, splitIndex).trim();
112
+ if (chunk.length > 0) {
113
+ chunks.push(chunk);
114
+ }
115
+ // Update remaining text
116
+ remaining = remaining.slice(splitIndex).trim();
117
+ }
118
+ return {
119
+ chunks,
120
+ wasSplit: chunks.length > 1,
121
+ originalLength,
122
+ };
123
+ }
124
+ /**
125
+ * Check if a message needs to be split
126
+ *
127
+ * @param content - Message content to check
128
+ * @param maxLength - Maximum message length (default: 2000)
129
+ * @returns true if the message exceeds the max length
130
+ */
131
+ export function needsSplit(content, maxLength = DISCORD_MAX_MESSAGE_LENGTH) {
132
+ return content.length > maxLength;
133
+ }
134
+ /**
135
+ * Start a typing indicator that refreshes automatically
136
+ *
137
+ * Discord typing indicators expire after ~10 seconds, so this function
138
+ * sets up an interval to keep refreshing the typing status until stopped.
139
+ *
140
+ * @param channel - Channel to show typing indicator in
141
+ * @param refreshInterval - How often to refresh (default: 5000ms)
142
+ * @returns Controller to stop the typing indicator
143
+ *
144
+ * @example
145
+ * ```typescript
146
+ * const typing = startTypingIndicator(channel);
147
+ * try {
148
+ * const response = await processMessage(prompt);
149
+ * await channel.send(response);
150
+ * } finally {
151
+ * typing.stop();
152
+ * }
153
+ * ```
154
+ */
155
+ export function startTypingIndicator(channel, refreshInterval = 5000) {
156
+ let isActive = true;
157
+ let intervalId = null;
158
+ // Send initial typing indicator
159
+ channel.sendTyping().catch(() => {
160
+ // Ignore errors - typing indicator is not critical
161
+ });
162
+ // Set up refresh interval
163
+ intervalId = setInterval(() => {
164
+ if (isActive) {
165
+ channel.sendTyping().catch(() => {
166
+ // Ignore errors
167
+ });
168
+ }
169
+ }, refreshInterval);
170
+ return {
171
+ stop() {
172
+ isActive = false;
173
+ if (intervalId !== null) {
174
+ clearInterval(intervalId);
175
+ intervalId = null;
176
+ }
177
+ },
178
+ get isActive() {
179
+ return isActive;
180
+ },
181
+ };
182
+ }
183
+ // =============================================================================
184
+ // Combined Send Utilities
185
+ // =============================================================================
186
+ /**
187
+ * Send a message, automatically splitting if needed
188
+ *
189
+ * @param channel - Channel to send the message to
190
+ * @param content - Message content to send
191
+ * @param options - Send options including split and delay settings
192
+ * @returns Array of message IDs for sent messages
193
+ *
194
+ * @example
195
+ * ```typescript
196
+ * const messageIds = await sendSplitMessage(channel, longResponse, {
197
+ * delayMs: 500,
198
+ * preserveBoundaries: true,
199
+ * });
200
+ * console.log(`Sent ${messageIds.length} messages`);
201
+ * ```
202
+ */
203
+ export async function sendSplitMessage(channel, content, options = {}) {
204
+ const { delayMs = DEFAULT_MESSAGE_DELAY_MS, ...splitOptions } = options;
205
+ const { chunks } = splitMessage(content, splitOptions);
206
+ const messageIds = [];
207
+ for (let i = 0; i < chunks.length; i++) {
208
+ const chunk = chunks[i];
209
+ // Send the message
210
+ const message = await channel.send(chunk);
211
+ messageIds.push(message.id);
212
+ // Add delay between messages (except after the last one)
213
+ if (i < chunks.length - 1 && delayMs > 0) {
214
+ await sleep(delayMs);
215
+ }
216
+ }
217
+ return messageIds;
218
+ }
219
+ /**
220
+ * Send a message with typing indicator
221
+ *
222
+ * Shows a typing indicator, processes the content, then sends the response.
223
+ * Automatically splits long messages.
224
+ *
225
+ * @param channel - Channel to send the message to
226
+ * @param contentProvider - Async function that generates the message content
227
+ * @param options - Send options
228
+ * @returns Array of message IDs for sent messages
229
+ *
230
+ * @example
231
+ * ```typescript
232
+ * const messageIds = await sendWithTyping(channel, async () => {
233
+ * return await generateResponse(prompt);
234
+ * });
235
+ * ```
236
+ */
237
+ export async function sendWithTyping(channel, contentProvider, options = {}) {
238
+ const typing = startTypingIndicator(channel);
239
+ try {
240
+ const content = await contentProvider();
241
+ return await sendSplitMessage(channel, content, options);
242
+ }
243
+ finally {
244
+ typing.stop();
245
+ }
246
+ }
247
+ // =============================================================================
248
+ // Utility Functions
249
+ // =============================================================================
250
+ /**
251
+ * Sleep for a specified duration
252
+ *
253
+ * @param ms - Duration in milliseconds
254
+ */
255
+ function sleep(ms) {
256
+ return new Promise((resolve) => setTimeout(resolve, ms));
257
+ }
258
+ /**
259
+ * Truncate a message to fit within the max length, adding an ellipsis
260
+ *
261
+ * @param content - Message content to truncate
262
+ * @param maxLength - Maximum length (default: 2000)
263
+ * @param ellipsis - Ellipsis to append (default: '...')
264
+ * @returns Truncated message
265
+ *
266
+ * @example
267
+ * ```typescript
268
+ * const short = truncateMessage(longText, 100);
269
+ * // Returns: "This is a very long text that has been trun..."
270
+ * ```
271
+ */
272
+ export function truncateMessage(content, maxLength = DISCORD_MAX_MESSAGE_LENGTH, ellipsis = "...") {
273
+ if (content.length <= maxLength) {
274
+ return content;
275
+ }
276
+ const truncatedLength = maxLength - ellipsis.length;
277
+ return content.slice(0, truncatedLength) + ellipsis;
278
+ }
279
+ /**
280
+ * Format code as a Discord code block
281
+ *
282
+ * @param code - Code to format
283
+ * @param language - Optional language for syntax highlighting
284
+ * @returns Formatted code block
285
+ *
286
+ * @example
287
+ * ```typescript
288
+ * const formatted = formatCodeBlock('const x = 1;', 'typescript');
289
+ * // Returns: "```typescript\nconst x = 1;\n```"
290
+ * ```
291
+ */
292
+ export function formatCodeBlock(code, language) {
293
+ const langTag = language ?? "";
294
+ return `\`\`\`${langTag}\n${code}\n\`\`\``;
295
+ }
296
+ /**
297
+ * Escape Discord markdown characters in text
298
+ *
299
+ * @param text - Text to escape
300
+ * @returns Text with markdown characters escaped
301
+ */
302
+ export function escapeMarkdown(text) {
303
+ return text.replace(/([*_~`|\\])/g, "\\$1");
304
+ }
305
+ //# sourceMappingURL=formatting.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"formatting.js","sourceRoot":"","sources":["../../src/utils/formatting.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Utility modules for the Discord connector
3
+ */
4
+ export { DISCORD_MAX_MESSAGE_LENGTH, DEFAULT_MESSAGE_DELAY_MS, MIN_CHUNK_SIZE, findSplitPoint, splitMessage, needsSplit, startTypingIndicator, sendSplitMessage, sendWithTyping, truncateMessage, formatCodeBlock, escapeMarkdown, type SendableChannel, type MessageSplitOptions, type SendSplitOptions, type SplitResult, type TypingController, } from "./formatting.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAEL,0BAA0B,EAC1B,wBAAwB,EACxB,cAAc,EAEd,cAAc,EACd,YAAY,EACZ,UAAU,EACV,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,eAAe,EACf,cAAc,EAEd,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Utility modules for the Discord connector
3
+ */
4
+ export {
5
+ // Constants
6
+ DISCORD_MAX_MESSAGE_LENGTH, DEFAULT_MESSAGE_DELAY_MS, MIN_CHUNK_SIZE,
7
+ // Functions
8
+ findSplitPoint, splitMessage, needsSplit, startTypingIndicator, sendSplitMessage, sendWithTyping, truncateMessage, formatCodeBlock, escapeMarkdown, } from "./formatting.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO;AACL,YAAY;AACZ,0BAA0B,EAC1B,wBAAwB,EACxB,cAAc;AACd,YAAY;AACZ,cAAc,EACd,YAAY,EACZ,UAAU,EACV,oBAAoB,EACpB,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,eAAe,EACf,cAAc,GAOf,MAAM,iBAAiB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@herdctl/discord",
3
+ "version": "0.0.1",
4
+ "description": "Discord connector for herdctl fleet management",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "dev": "tsc --watch",
15
+ "typecheck": "tsc --noEmit",
16
+ "test": "vitest run --coverage"
17
+ },
18
+ "dependencies": {
19
+ "@discordjs/rest": "^2.6.0",
20
+ "@herdctl/core": "workspace:*",
21
+ "discord.js": "^14.16.0",
22
+ "yaml": "^2.3.0",
23
+ "zod": "^3.22.0"
24
+ },
25
+ "devDependencies": {
26
+ "@vitest/coverage-v8": "^4.0.17",
27
+ "typescript": "^5",
28
+ "vitest": "^4.0.17"
29
+ },
30
+ "homepage": "https://herdctl.dev",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/edspencer/herdctl"
34
+ },
35
+ "keywords": [
36
+ "claude",
37
+ "agent",
38
+ "fleet",
39
+ "orchestration",
40
+ "anthropic",
41
+ "discord"
42
+ ],
43
+ "engines": {
44
+ "node": ">=18"
45
+ },
46
+ "publishConfig": {
47
+ "access": "public"
48
+ }
49
+ }