@animalabs/agent-framework 0.1.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 (212) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/src/agent.d.ts +130 -0
  4. package/dist/src/agent.d.ts.map +1 -0
  5. package/dist/src/agent.js +406 -0
  6. package/dist/src/agent.js.map +1 -0
  7. package/dist/src/api/mcp-server.d.ts +37 -0
  8. package/dist/src/api/mcp-server.d.ts.map +1 -0
  9. package/dist/src/api/mcp-server.js +598 -0
  10. package/dist/src/api/mcp-server.js.map +1 -0
  11. package/dist/src/api/server.d.ts +83 -0
  12. package/dist/src/api/server.d.ts.map +1 -0
  13. package/dist/src/api/server.js +785 -0
  14. package/dist/src/api/server.js.map +1 -0
  15. package/dist/src/api/types.d.ts +395 -0
  16. package/dist/src/api/types.d.ts.map +1 -0
  17. package/dist/src/api/types.js +5 -0
  18. package/dist/src/api/types.js.map +1 -0
  19. package/dist/src/framework.d.ts +309 -0
  20. package/dist/src/framework.d.ts.map +1 -0
  21. package/dist/src/framework.js +2309 -0
  22. package/dist/src/framework.js.map +1 -0
  23. package/dist/src/gate/event-gate.d.ts +75 -0
  24. package/dist/src/gate/event-gate.d.ts.map +1 -0
  25. package/dist/src/gate/event-gate.js +503 -0
  26. package/dist/src/gate/event-gate.js.map +1 -0
  27. package/dist/src/gate/index.d.ts +3 -0
  28. package/dist/src/gate/index.d.ts.map +1 -0
  29. package/dist/src/gate/index.js +2 -0
  30. package/dist/src/gate/index.js.map +1 -0
  31. package/dist/src/gate/types.d.ts +82 -0
  32. package/dist/src/gate/types.d.ts.map +1 -0
  33. package/dist/src/gate/types.js +9 -0
  34. package/dist/src/gate/types.js.map +1 -0
  35. package/dist/src/index.d.ts +17 -0
  36. package/dist/src/index.d.ts.map +1 -0
  37. package/dist/src/index.js +18 -0
  38. package/dist/src/index.js.map +1 -0
  39. package/dist/src/mcpl/channel-registry.d.ts +141 -0
  40. package/dist/src/mcpl/channel-registry.d.ts.map +1 -0
  41. package/dist/src/mcpl/channel-registry.js +647 -0
  42. package/dist/src/mcpl/channel-registry.js.map +1 -0
  43. package/dist/src/mcpl/checkpoint-manager.d.ts +79 -0
  44. package/dist/src/mcpl/checkpoint-manager.d.ts.map +1 -0
  45. package/dist/src/mcpl/checkpoint-manager.js +430 -0
  46. package/dist/src/mcpl/checkpoint-manager.js.map +1 -0
  47. package/dist/src/mcpl/errors.d.ts +52 -0
  48. package/dist/src/mcpl/errors.d.ts.map +1 -0
  49. package/dist/src/mcpl/errors.js +93 -0
  50. package/dist/src/mcpl/errors.js.map +1 -0
  51. package/dist/src/mcpl/feature-set-manager.d.ts +91 -0
  52. package/dist/src/mcpl/feature-set-manager.d.ts.map +1 -0
  53. package/dist/src/mcpl/feature-set-manager.js +250 -0
  54. package/dist/src/mcpl/feature-set-manager.js.map +1 -0
  55. package/dist/src/mcpl/hook-orchestrator.d.ts +51 -0
  56. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -0
  57. package/dist/src/mcpl/hook-orchestrator.js +217 -0
  58. package/dist/src/mcpl/hook-orchestrator.js.map +1 -0
  59. package/dist/src/mcpl/index.d.ts +14 -0
  60. package/dist/src/mcpl/index.d.ts.map +1 -0
  61. package/dist/src/mcpl/index.js +21 -0
  62. package/dist/src/mcpl/index.js.map +1 -0
  63. package/dist/src/mcpl/inference-router.d.ts +67 -0
  64. package/dist/src/mcpl/inference-router.d.ts.map +1 -0
  65. package/dist/src/mcpl/inference-router.js +249 -0
  66. package/dist/src/mcpl/inference-router.js.map +1 -0
  67. package/dist/src/mcpl/push-handler.d.ts +60 -0
  68. package/dist/src/mcpl/push-handler.d.ts.map +1 -0
  69. package/dist/src/mcpl/push-handler.js +162 -0
  70. package/dist/src/mcpl/push-handler.js.map +1 -0
  71. package/dist/src/mcpl/scope-manager.d.ts +76 -0
  72. package/dist/src/mcpl/scope-manager.d.ts.map +1 -0
  73. package/dist/src/mcpl/scope-manager.js +213 -0
  74. package/dist/src/mcpl/scope-manager.js.map +1 -0
  75. package/dist/src/mcpl/server-connection.d.ts +174 -0
  76. package/dist/src/mcpl/server-connection.d.ts.map +1 -0
  77. package/dist/src/mcpl/server-connection.js +559 -0
  78. package/dist/src/mcpl/server-connection.js.map +1 -0
  79. package/dist/src/mcpl/server-registry.d.ts +63 -0
  80. package/dist/src/mcpl/server-registry.d.ts.map +1 -0
  81. package/dist/src/mcpl/server-registry.js +119 -0
  82. package/dist/src/mcpl/server-registry.js.map +1 -0
  83. package/dist/src/mcpl/types.d.ts +755 -0
  84. package/dist/src/mcpl/types.d.ts.map +1 -0
  85. package/dist/src/mcpl/types.js +48 -0
  86. package/dist/src/mcpl/types.js.map +1 -0
  87. package/dist/src/module-registry.d.ts +93 -0
  88. package/dist/src/module-registry.d.ts.map +1 -0
  89. package/dist/src/module-registry.js +386 -0
  90. package/dist/src/module-registry.js.map +1 -0
  91. package/dist/src/modules/api/index.d.ts +22 -0
  92. package/dist/src/modules/api/index.d.ts.map +1 -0
  93. package/dist/src/modules/api/index.js +63 -0
  94. package/dist/src/modules/api/index.js.map +1 -0
  95. package/dist/src/modules/discord/discord-js-client.d.ts +58 -0
  96. package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -0
  97. package/dist/src/modules/discord/discord-js-client.js +273 -0
  98. package/dist/src/modules/discord/discord-js-client.js.map +1 -0
  99. package/dist/src/modules/discord/index.d.ts +82 -0
  100. package/dist/src/modules/discord/index.d.ts.map +1 -0
  101. package/dist/src/modules/discord/index.js +959 -0
  102. package/dist/src/modules/discord/index.js.map +1 -0
  103. package/dist/src/modules/discord/types.d.ts +327 -0
  104. package/dist/src/modules/discord/types.d.ts.map +1 -0
  105. package/dist/src/modules/discord/types.js +5 -0
  106. package/dist/src/modules/discord/types.js.map +1 -0
  107. package/dist/src/modules/index.d.ts +11 -0
  108. package/dist/src/modules/index.d.ts.map +1 -0
  109. package/dist/src/modules/index.js +7 -0
  110. package/dist/src/modules/index.js.map +1 -0
  111. package/dist/src/modules/workspace/index.d.ts +59 -0
  112. package/dist/src/modules/workspace/index.d.ts.map +1 -0
  113. package/dist/src/modules/workspace/index.js +831 -0
  114. package/dist/src/modules/workspace/index.js.map +1 -0
  115. package/dist/src/modules/workspace/sync.d.ts +48 -0
  116. package/dist/src/modules/workspace/sync.d.ts.map +1 -0
  117. package/dist/src/modules/workspace/sync.js +253 -0
  118. package/dist/src/modules/workspace/sync.js.map +1 -0
  119. package/dist/src/modules/workspace/types.d.ts +166 -0
  120. package/dist/src/modules/workspace/types.d.ts.map +1 -0
  121. package/dist/src/modules/workspace/types.js +5 -0
  122. package/dist/src/modules/workspace/types.js.map +1 -0
  123. package/dist/src/modules/workspace/watcher.d.ts +43 -0
  124. package/dist/src/modules/workspace/watcher.d.ts.map +1 -0
  125. package/dist/src/modules/workspace/watcher.js +120 -0
  126. package/dist/src/modules/workspace/watcher.js.map +1 -0
  127. package/dist/src/queue.d.ts +50 -0
  128. package/dist/src/queue.d.ts.map +1 -0
  129. package/dist/src/queue.js +95 -0
  130. package/dist/src/queue.js.map +1 -0
  131. package/dist/src/safe-slice.d.ts +8 -0
  132. package/dist/src/safe-slice.d.ts.map +1 -0
  133. package/dist/src/safe-slice.js +18 -0
  134. package/dist/src/safe-slice.js.map +1 -0
  135. package/dist/src/types/agent.d.ts +118 -0
  136. package/dist/src/types/agent.d.ts.map +1 -0
  137. package/dist/src/types/agent.js +2 -0
  138. package/dist/src/types/agent.js.map +1 -0
  139. package/dist/src/types/events.d.ts +220 -0
  140. package/dist/src/types/events.d.ts.map +1 -0
  141. package/dist/src/types/events.js +2 -0
  142. package/dist/src/types/events.js.map +1 -0
  143. package/dist/src/types/framework.d.ts +184 -0
  144. package/dist/src/types/framework.d.ts.map +1 -0
  145. package/dist/src/types/framework.js +2 -0
  146. package/dist/src/types/framework.js.map +1 -0
  147. package/dist/src/types/index.d.ts +7 -0
  148. package/dist/src/types/index.d.ts.map +1 -0
  149. package/dist/src/types/index.js +7 -0
  150. package/dist/src/types/index.js.map +1 -0
  151. package/dist/src/types/inference-log.d.ts +64 -0
  152. package/dist/src/types/inference-log.d.ts.map +1 -0
  153. package/dist/src/types/inference-log.js +5 -0
  154. package/dist/src/types/inference-log.js.map +1 -0
  155. package/dist/src/types/module.d.ts +302 -0
  156. package/dist/src/types/module.d.ts.map +1 -0
  157. package/dist/src/types/module.js +2 -0
  158. package/dist/src/types/module.js.map +1 -0
  159. package/dist/src/types/trace.d.ts +173 -0
  160. package/dist/src/types/trace.d.ts.map +1 -0
  161. package/dist/src/types/trace.js +2 -0
  162. package/dist/src/types/trace.js.map +1 -0
  163. package/dist/test/event-gate.test.d.ts +2 -0
  164. package/dist/test/event-gate.test.d.ts.map +1 -0
  165. package/dist/test/event-gate.test.js +535 -0
  166. package/dist/test/event-gate.test.js.map +1 -0
  167. package/dist/test/framework.test.d.ts +5 -0
  168. package/dist/test/framework.test.d.ts.map +1 -0
  169. package/dist/test/framework.test.js +738 -0
  170. package/dist/test/framework.test.js.map +1 -0
  171. package/dist/tsconfig.tsbuildinfo +1 -0
  172. package/package.json +58 -0
  173. package/src/agent.ts +512 -0
  174. package/src/api/mcp-server.ts +687 -0
  175. package/src/api/server.ts +1023 -0
  176. package/src/api/types.ts +542 -0
  177. package/src/framework.ts +2784 -0
  178. package/src/gate/event-gate.ts +623 -0
  179. package/src/gate/index.ts +12 -0
  180. package/src/gate/types.ts +112 -0
  181. package/src/index.ts +63 -0
  182. package/src/mcpl/channel-registry.ts +824 -0
  183. package/src/mcpl/checkpoint-manager.ts +545 -0
  184. package/src/mcpl/errors.ts +114 -0
  185. package/src/mcpl/feature-set-manager.ts +333 -0
  186. package/src/mcpl/hook-orchestrator.ts +281 -0
  187. package/src/mcpl/index.ts +134 -0
  188. package/src/mcpl/inference-router.ts +335 -0
  189. package/src/mcpl/push-handler.ts +232 -0
  190. package/src/mcpl/scope-manager.ts +262 -0
  191. package/src/mcpl/server-connection.ts +694 -0
  192. package/src/mcpl/server-registry.ts +144 -0
  193. package/src/mcpl/types.ts +991 -0
  194. package/src/module-registry.ts +547 -0
  195. package/src/modules/api/index.ts +94 -0
  196. package/src/modules/discord/discord-js-client.ts +353 -0
  197. package/src/modules/discord/index.ts +1146 -0
  198. package/src/modules/discord/types.ts +465 -0
  199. package/src/modules/index.ts +43 -0
  200. package/src/modules/workspace/index.ts +969 -0
  201. package/src/modules/workspace/sync.ts +314 -0
  202. package/src/modules/workspace/types.ts +202 -0
  203. package/src/modules/workspace/watcher.ts +142 -0
  204. package/src/queue.ts +106 -0
  205. package/src/safe-slice.ts +18 -0
  206. package/src/types/agent.ts +119 -0
  207. package/src/types/events.ts +252 -0
  208. package/src/types/framework.ts +211 -0
  209. package/src/types/index.ts +6 -0
  210. package/src/types/inference-log.ts +64 -0
  211. package/src/types/module.ts +367 -0
  212. package/src/types/trace.ts +196 -0
@@ -0,0 +1,959 @@
1
+ /**
2
+ * Discord Module - Discord integration for agent framework
3
+ *
4
+ * Provides:
5
+ * - Listening to Discord messages and routing to agents
6
+ * - Sending agent speech to Discord channels
7
+ * - Tools for explicit Discord operations (send, DM, react, etc.)
8
+ */
9
+ export * from './types.js';
10
+ export { DiscordJsClient } from './discord-js-client.js';
11
+ const DEFAULT_CONFIG = {
12
+ handleDMs: true,
13
+ ignoreBots: true,
14
+ rateLimitPerMinute: 30,
15
+ triggerOn: 'mention_or_reply',
16
+ autoTyping: true,
17
+ historyScrollback: 50,
18
+ };
19
+ // Image MIME types that Claude can process
20
+ const SUPPORTED_IMAGE_TYPES = [
21
+ 'image/jpeg',
22
+ 'image/png',
23
+ 'image/gif',
24
+ 'image/webp',
25
+ ];
26
+ // Text file extensions for inline reading
27
+ const TEXT_FILE_EXTENSIONS = [
28
+ '.txt', '.md', '.json', '.yaml', '.yml', '.toml',
29
+ '.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.go', '.rs',
30
+ '.html', '.css', '.xml', '.csv', '.log', '.sh', '.bash',
31
+ ];
32
+ const CONVERSATION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
33
+ /**
34
+ * Discord module for the agent framework.
35
+ */
36
+ export class DiscordModule {
37
+ name = 'discord';
38
+ config;
39
+ ctx = null;
40
+ client;
41
+ state = {
42
+ connectedGuilds: [],
43
+ conversations: {},
44
+ rateLimits: {},
45
+ lastReadMessageId: {},
46
+ };
47
+ // Track current reply context per agent
48
+ replyContexts = new Map();
49
+ // Last message sent by a tool (for thought-editing)
50
+ lastSentMessage = null;
51
+ constructor(client, config) {
52
+ this.client = client;
53
+ this.config = { ...DEFAULT_CONFIG, ...config };
54
+ }
55
+ async start(ctx) {
56
+ this.ctx = ctx;
57
+ // Load persisted state
58
+ const savedState = ctx.getState();
59
+ if (savedState) {
60
+ this.state = savedState;
61
+ // Clean up old conversations
62
+ this.cleanupOldConversations();
63
+ }
64
+ // Register as speech handler
65
+ ctx.registerSpeechHandler('*');
66
+ // Set up Discord event handlers
67
+ this.client.onMessage((message) => this.handleDiscordMessage(message));
68
+ this.client.onMessageEdit((id, content) => this.handleDiscordMessageEdit(id, content));
69
+ this.client.onMessageDelete((id) => this.handleDiscordMessageDelete(id));
70
+ // Set up ready handler for history sync on (re)connect
71
+ if (this.client.onReady) {
72
+ this.client.onReady(() => this.handleReady());
73
+ }
74
+ // Connect to Discord
75
+ if (!ctx.isRestart || !this.client.isConnected()) {
76
+ await this.client.connect();
77
+ }
78
+ else {
79
+ // Already connected (restart) - sync history now
80
+ await this.syncAllChannelHistory();
81
+ }
82
+ // Get bot user ID after connect
83
+ const botUserId = this.client.getBotUserId();
84
+ if (botUserId) {
85
+ this.state.botUserId = botUserId;
86
+ ctx.setState(this.state);
87
+ }
88
+ }
89
+ async stop() {
90
+ // Save state
91
+ if (this.ctx) {
92
+ this.ctx.setState(this.state);
93
+ }
94
+ await this.client.disconnect();
95
+ this.ctx = null;
96
+ }
97
+ getTools() {
98
+ return [
99
+ {
100
+ name: 'send',
101
+ description: 'Send a message to a specific Discord channel',
102
+ inputSchema: {
103
+ type: 'object',
104
+ properties: {
105
+ channelId: { type: 'string', description: 'Channel ID to send to' },
106
+ content: { type: 'string', description: 'Message content' },
107
+ replyTo: { type: 'string', description: 'Message ID to reply to (optional)' },
108
+ createThread: {
109
+ type: 'object',
110
+ description: 'Create a thread with this message',
111
+ properties: {
112
+ name: { type: 'string', description: 'Thread name' },
113
+ },
114
+ },
115
+ },
116
+ required: ['channelId', 'content'],
117
+ },
118
+ },
119
+ {
120
+ name: 'dm',
121
+ description: 'Send a direct message to a user',
122
+ inputSchema: {
123
+ type: 'object',
124
+ properties: {
125
+ userId: { type: 'string', description: 'User ID to DM' },
126
+ content: { type: 'string', description: 'Message content' },
127
+ },
128
+ required: ['userId', 'content'],
129
+ },
130
+ },
131
+ {
132
+ name: 'reply',
133
+ description: 'Reply in the current conversation context. Use this for normal responses.',
134
+ inputSchema: {
135
+ type: 'object',
136
+ properties: {
137
+ content: { type: 'string', description: 'Message content' },
138
+ mention: { type: 'boolean', description: 'Whether to mention the user' },
139
+ },
140
+ required: ['content'],
141
+ },
142
+ },
143
+ {
144
+ name: 'react',
145
+ description: 'Add a reaction to a message',
146
+ inputSchema: {
147
+ type: 'object',
148
+ properties: {
149
+ channelId: { type: 'string', description: 'Channel ID' },
150
+ messageId: { type: 'string', description: 'Message ID to react to' },
151
+ emoji: { type: 'string', description: 'Emoji (unicode or custom emoji ID)' },
152
+ },
153
+ required: ['channelId', 'messageId', 'emoji'],
154
+ },
155
+ },
156
+ {
157
+ name: 'edit',
158
+ description: 'Edit a message sent by this bot',
159
+ inputSchema: {
160
+ type: 'object',
161
+ properties: {
162
+ channelId: { type: 'string', description: 'Channel ID' },
163
+ messageId: { type: 'string', description: 'Message ID to edit' },
164
+ content: { type: 'string', description: 'New content' },
165
+ },
166
+ required: ['channelId', 'messageId', 'content'],
167
+ },
168
+ },
169
+ {
170
+ name: 'delete',
171
+ description: 'Delete a message sent by this bot',
172
+ inputSchema: {
173
+ type: 'object',
174
+ properties: {
175
+ channelId: { type: 'string', description: 'Channel ID' },
176
+ messageId: { type: 'string', description: 'Message ID to delete' },
177
+ },
178
+ required: ['channelId', 'messageId'],
179
+ },
180
+ },
181
+ {
182
+ name: 'create_thread',
183
+ description: 'Create a thread from a message',
184
+ inputSchema: {
185
+ type: 'object',
186
+ properties: {
187
+ channelId: { type: 'string', description: 'Channel ID' },
188
+ messageId: { type: 'string', description: 'Message ID to create thread from' },
189
+ name: { type: 'string', description: 'Thread name' },
190
+ autoArchiveDuration: {
191
+ type: 'number',
192
+ description: 'Auto-archive duration in minutes (60, 1440, 4320, 10080)',
193
+ },
194
+ },
195
+ required: ['channelId', 'messageId', 'name'],
196
+ },
197
+ },
198
+ {
199
+ name: 'set_reply_context',
200
+ description: 'Change where default speech goes for this conversation',
201
+ inputSchema: {
202
+ type: 'object',
203
+ properties: {
204
+ channelId: { type: 'string', description: 'Channel ID for replies' },
205
+ threadId: { type: 'string', description: 'Thread ID (optional)' },
206
+ replyToMessageId: { type: 'string', description: 'Message ID to reply to (optional)' },
207
+ },
208
+ required: ['channelId'],
209
+ },
210
+ },
211
+ {
212
+ name: 'activate',
213
+ description: 'Register Discord as the speech handler (take over from other modules)',
214
+ inputSchema: {
215
+ type: 'object',
216
+ properties: {
217
+ additive: {
218
+ type: 'boolean',
219
+ description: 'If true, add to existing handlers instead of replacing',
220
+ },
221
+ },
222
+ },
223
+ },
224
+ {
225
+ name: 'deactivate',
226
+ description: 'Unregister Discord as a speech handler',
227
+ inputSchema: {
228
+ type: 'object',
229
+ properties: {},
230
+ },
231
+ },
232
+ // ========== Discovery & Utility Tools ==========
233
+ {
234
+ name: 'list_guilds',
235
+ description: 'List all Discord servers (guilds) the bot is in',
236
+ inputSchema: {
237
+ type: 'object',
238
+ properties: {},
239
+ },
240
+ },
241
+ {
242
+ name: 'list_channels',
243
+ description: 'List all channels in a Discord server',
244
+ inputSchema: {
245
+ type: 'object',
246
+ properties: {
247
+ guildId: { type: 'string', description: 'Guild/Server ID' },
248
+ },
249
+ required: ['guildId'],
250
+ },
251
+ },
252
+ {
253
+ name: 'fetch_history',
254
+ description: 'Fetch recent message history from a channel',
255
+ inputSchema: {
256
+ type: 'object',
257
+ properties: {
258
+ channelId: { type: 'string', description: 'Channel ID' },
259
+ limit: { type: 'number', description: 'Number of messages to fetch (default: 50, max: 100)' },
260
+ before: { type: 'string', description: 'Fetch messages before this message ID' },
261
+ },
262
+ required: ['channelId'],
263
+ },
264
+ },
265
+ ];
266
+ }
267
+ async handleToolCall(call) {
268
+ try {
269
+ switch (call.name) {
270
+ case 'send':
271
+ return await this.handleSend(call.input);
272
+ case 'dm':
273
+ return await this.handleDM(call.input);
274
+ case 'reply':
275
+ return await this.handleReply(call.input);
276
+ case 'react':
277
+ return await this.handleReact(call.input);
278
+ case 'edit':
279
+ return await this.handleEdit(call.input);
280
+ case 'delete':
281
+ return await this.handleDelete(call.input);
282
+ case 'create_thread':
283
+ return await this.handleCreateThread(call.input);
284
+ case 'set_reply_context':
285
+ return await this.handleSetReplyContext(call.input);
286
+ case 'activate':
287
+ return await this.handleActivate(call.input);
288
+ case 'deactivate':
289
+ return await this.handleDeactivate();
290
+ case 'list_guilds':
291
+ return await this.handleListGuilds();
292
+ case 'list_channels':
293
+ return await this.handleListChannels(call.input);
294
+ case 'fetch_history':
295
+ return await this.handleFetchHistory(call.input);
296
+ default:
297
+ return { success: false, error: `Unknown tool: ${call.name}`, isError: true };
298
+ }
299
+ }
300
+ catch (error) {
301
+ return {
302
+ success: false,
303
+ error: error instanceof Error ? error.message : String(error),
304
+ isError: true,
305
+ };
306
+ }
307
+ }
308
+ async onProcess(event, state) {
309
+ // Handle new Discord message
310
+ if (event.type === 'discord:message') {
311
+ const msg = event;
312
+ const currentState = state.getState() ?? {
313
+ connectedGuilds: [],
314
+ conversations: {},
315
+ rateLimits: {},
316
+ lastReadMessageId: {},
317
+ };
318
+ // Build conversation state update
319
+ const convKey = msg.guildId ? `${msg.guildId}:${msg.channelId}` : `dm:${msg.authorId}`;
320
+ const conversation = {
321
+ channelId: msg.channelId,
322
+ userId: msg.authorId,
323
+ guildId: msg.guildId ?? null,
324
+ replyToMessageId: msg.discordMessageId,
325
+ lastActivity: Date.now(),
326
+ };
327
+ // Update in-memory reply context (ephemeral, not persisted)
328
+ this.replyContexts.set('default', conversation);
329
+ // Build new state with updated conversation
330
+ const newState = {
331
+ ...currentState,
332
+ conversations: {
333
+ ...currentState.conversations,
334
+ [convKey]: conversation,
335
+ },
336
+ lastReadMessageId: {
337
+ ...currentState.lastReadMessageId,
338
+ [msg.channelId]: msg.discordMessageId,
339
+ },
340
+ };
341
+ // Update local state reference for tool handlers
342
+ this.state = newState;
343
+ // Auto-send typing indicator if configured
344
+ if (this.config.autoTyping) {
345
+ // Fire and forget - don't await
346
+ this.client.sendTyping(msg.channelId).catch((err) => {
347
+ console.warn('Discord: Failed to send typing indicator:', err);
348
+ });
349
+ }
350
+ // Build content blocks from message and attachments
351
+ const contentBlocks = msg.contentBlocks ?? [{ type: 'text', text: msg.content }];
352
+ // Return declarative response - framework applies all writes atomically
353
+ return {
354
+ addMessages: [
355
+ {
356
+ participant: msg.authorName,
357
+ content: contentBlocks,
358
+ metadata: {
359
+ external: { source: 'discord', id: msg.discordMessageId },
360
+ channelId: msg.channelId,
361
+ timestamp: msg.timestamp,
362
+ },
363
+ },
364
+ ],
365
+ stateUpdate: newState,
366
+ requestInference: msg.shouldTriggerInference ?? true,
367
+ };
368
+ }
369
+ // Handle Discord message edit
370
+ if (event.type === 'discord:edit') {
371
+ const edit = event;
372
+ const internalId = state.findMessageByExternalId('discord', edit.discordMessageId);
373
+ if (!internalId) {
374
+ return {};
375
+ }
376
+ return {
377
+ editMessages: [
378
+ {
379
+ messageId: internalId,
380
+ content: [{ type: 'text', text: edit.newContent }],
381
+ },
382
+ ],
383
+ };
384
+ }
385
+ // Handle Discord message delete
386
+ if (event.type === 'discord:delete') {
387
+ const del = event;
388
+ const internalId = state.findMessageByExternalId('discord', del.discordMessageId);
389
+ if (!internalId) {
390
+ return {};
391
+ }
392
+ return {
393
+ removeMessages: [internalId],
394
+ };
395
+ }
396
+ return {};
397
+ }
398
+ /**
399
+ * Handle agent speech - send to Discord.
400
+ */
401
+ async onAgentSpeech(agentName, content, context) {
402
+ // If thoughts are present and we have a recently sent message, edit it
403
+ // to prepend the thoughts as spoiler text.
404
+ if (context.thoughts && context.thoughts.length > 0 && this.lastSentMessage) {
405
+ const thoughtText = this.extractText(context.thoughts);
406
+ if (thoughtText) {
407
+ try {
408
+ const { channelId, messageId } = this.lastSentMessage;
409
+ // Fetch the current message content via the client isn't available,
410
+ // but we know the tool just sent it — reconstruct by editing with
411
+ // spoiler-wrapped thoughts prepended.
412
+ // Discord API edit replaces content, so we need the original text.
413
+ // We don't have it here, so use a get-then-edit pattern via the API.
414
+ // For now, the simplest approach: the discord.js client can fetch & edit.
415
+ await this.editWithThoughts(channelId, messageId, thoughtText);
416
+ }
417
+ catch (err) {
418
+ console.warn('Discord: Failed to edit thoughts into message:', err);
419
+ }
420
+ }
421
+ this.lastSentMessage = null;
422
+ }
423
+ // Get reply context for this agent
424
+ const replyCtx = this.replyContexts.get(agentName) ?? this.replyContexts.get('default');
425
+ if (!replyCtx) {
426
+ // No conversation context - nowhere to send
427
+ console.warn(`Discord: No reply context for agent ${agentName}`);
428
+ return;
429
+ }
430
+ // Extract text content
431
+ const text = this.extractText(content);
432
+ if (!text) {
433
+ return;
434
+ }
435
+ // Check rate limit
436
+ if (!this.checkRateLimit(replyCtx.channelId)) {
437
+ console.warn(`Discord: Rate limited for channel ${replyCtx.channelId}`);
438
+ return;
439
+ }
440
+ // Send to Discord
441
+ const targetChannel = replyCtx.threadId ?? replyCtx.channelId;
442
+ const result = await this.client.sendMessage(targetChannel, text, {
443
+ replyTo: replyCtx.replyToMessageId,
444
+ });
445
+ // Track for thought-editing (in case of chained speech)
446
+ this.lastSentMessage = { channelId: targetChannel, messageId: result.messageId };
447
+ // Clear reply-to after sending (don't keep replying to same message)
448
+ replyCtx.replyToMessageId = undefined;
449
+ replyCtx.lastActivity = Date.now();
450
+ }
451
+ /**
452
+ * Edit a sent message to prepend thought text as spoilers.
453
+ */
454
+ async editWithThoughts(channelId, messageId, thoughtText) {
455
+ // Fetch current message content
456
+ const currentContent = await this.client.fetchMessage(channelId, messageId);
457
+ // Prepend spoiler-wrapped thoughts
458
+ const spoiler = `||${thoughtText}||\n`;
459
+ await this.client.editMessage(channelId, messageId, spoiler + currentContent);
460
+ }
461
+ // ==========================================================================
462
+ // Discord Event Handlers
463
+ // ==========================================================================
464
+ /**
465
+ * Handle Discord ready event - sync history for all tracked channels.
466
+ */
467
+ async handleReady() {
468
+ console.log('[Discord] Ready - syncing history for tracked channels');
469
+ // Get bot user ID
470
+ const botUserId = this.client.getBotUserId();
471
+ if (botUserId) {
472
+ this.state.botUserId = botUserId;
473
+ }
474
+ await this.syncAllChannelHistory();
475
+ }
476
+ /**
477
+ * Sync history for all channels we have conversations in.
478
+ */
479
+ async syncAllChannelHistory() {
480
+ if (!this.ctx)
481
+ return;
482
+ // Get unique channel IDs from conversations
483
+ const channelIds = new Set();
484
+ for (const conv of Object.values(this.state.conversations)) {
485
+ channelIds.add(conv.channelId);
486
+ }
487
+ if (channelIds.size === 0) {
488
+ console.log('[Discord] No channels to sync');
489
+ return;
490
+ }
491
+ console.log(`[Discord] Syncing history for ${channelIds.size} channel(s)`);
492
+ const syncResults = [];
493
+ for (const channelId of channelIds) {
494
+ try {
495
+ const result = await this.syncChannelHistory(channelId);
496
+ if (result.newMessages > 0 || result.editedMessages > 0 || result.deletedMessages > 0) {
497
+ syncResults.push({ channelId, ...result });
498
+ }
499
+ }
500
+ catch (error) {
501
+ console.error(`[Discord] Failed to sync channel ${channelId}:`, error);
502
+ }
503
+ }
504
+ // Emit sync complete event if there were any changes
505
+ if (syncResults.length > 0 && this.ctx) {
506
+ const totalNew = syncResults.reduce((sum, r) => sum + r.newMessages, 0);
507
+ const totalEdited = syncResults.reduce((sum, r) => sum + r.editedMessages, 0);
508
+ const totalDeleted = syncResults.reduce((sum, r) => sum + r.deletedMessages, 0);
509
+ console.log(`[Discord] History sync complete: ${totalNew} new, ${totalEdited} edited, ${totalDeleted} deleted`);
510
+ // Push a notification event (doesn't trigger inference, just informs)
511
+ this.ctx.pushEvent({
512
+ type: 'module-event',
513
+ source: 'discord',
514
+ eventType: 'history-sync-complete',
515
+ payload: {
516
+ channels: syncResults.length,
517
+ newMessages: totalNew,
518
+ editedMessages: totalEdited,
519
+ deletedMessages: totalDeleted,
520
+ },
521
+ });
522
+ }
523
+ this.ctx?.setState(this.state);
524
+ }
525
+ /**
526
+ * Sync history for a specific channel - compare with Context Manager and update.
527
+ * Uses queryMessages() to enumerate stored messages for proper edit/delete detection.
528
+ */
529
+ async syncChannelHistory(channelId) {
530
+ if (!this.ctx) {
531
+ return { newMessages: 0, editedMessages: 0, deletedMessages: 0 };
532
+ }
533
+ const scrollback = this.config.historyScrollback ?? 50;
534
+ // Fetch recent history from Discord
535
+ const discordMessages = await this.client.fetchHistory(channelId, { limit: scrollback });
536
+ // Build a map of Discord message ID -> content for comparison
537
+ const discordMessageMap = new Map();
538
+ for (const msg of discordMessages) {
539
+ discordMessageMap.set(msg.id, {
540
+ content: msg.content,
541
+ authorName: msg.authorName,
542
+ timestamp: msg.timestamp,
543
+ });
544
+ }
545
+ // Query Context Manager for all Discord messages from this channel
546
+ const { messages: storedMessages } = this.ctx.queryMessages({
547
+ source: 'discord',
548
+ metadata: { channelId },
549
+ });
550
+ // Build a map of stored message external IDs -> internal data
551
+ const storedMessageMap = new Map();
552
+ for (const msg of storedMessages) {
553
+ const external = msg.metadata?.external;
554
+ if (external?.id) {
555
+ // Extract text content for comparison
556
+ const textContent = msg.content
557
+ .filter((block) => block.type === 'text')
558
+ .map((block) => block.text)
559
+ .join('\n');
560
+ storedMessageMap.set(external.id, {
561
+ internalId: msg.id,
562
+ content: textContent,
563
+ });
564
+ }
565
+ }
566
+ let newMessages = 0;
567
+ let editedMessages = 0;
568
+ let deletedMessages = 0;
569
+ // Process Discord messages - check for new/edited
570
+ for (const discordMsg of discordMessages) {
571
+ const stored = storedMessageMap.get(discordMsg.id);
572
+ if (!stored) {
573
+ // New message we haven't seen
574
+ // For bot's own messages (from previous sessions), use the agent name
575
+ // This ensures continuity of conversation history
576
+ const isBotMessage = discordMsg.authorId === this.state.botUserId;
577
+ const agentName = this.ctx.getAgents()[0]?.name;
578
+ const participantName = isBotMessage
579
+ ? (agentName ?? discordMsg.authorName)
580
+ : discordMsg.authorName;
581
+ this.ctx.addMessage(participantName, [{ type: 'text', text: discordMsg.content }], {
582
+ external: { source: 'discord', id: discordMsg.id },
583
+ channelId,
584
+ timestamp: discordMsg.timestamp.getTime(),
585
+ isOwnMessage: isBotMessage, // Mark as our own for reference
586
+ });
587
+ newMessages++;
588
+ }
589
+ else {
590
+ // We have this message - check if content changed (offline edit detection)
591
+ if (stored.content !== discordMsg.content) {
592
+ this.ctx.editMessage(stored.internalId, [
593
+ { type: 'text', text: discordMsg.content },
594
+ ]);
595
+ editedMessages++;
596
+ }
597
+ }
598
+ }
599
+ // Check for deletes - messages we have that Discord doesn't
600
+ // Only consider messages within the scrollback window (recent ones)
601
+ for (const [externalId, stored] of storedMessageMap) {
602
+ if (!discordMessageMap.has(externalId)) {
603
+ // Message exists in our store but not in Discord - it was deleted
604
+ // Note: This only catches deletes within the scrollback window
605
+ this.ctx.removeMessage(stored.internalId);
606
+ deletedMessages++;
607
+ }
608
+ }
609
+ // Update last read
610
+ if (discordMessages.length > 0) {
611
+ // Messages are typically newest first
612
+ const newestId = discordMessages[0].id;
613
+ this.state.lastReadMessageId[channelId] = newestId;
614
+ }
615
+ return { newMessages, editedMessages, deletedMessages };
616
+ }
617
+ async handleDiscordMessage(message) {
618
+ if (!this.ctx)
619
+ return;
620
+ // ALWAYS ignore our own messages to prevent feedback loops
621
+ if (this.state.botUserId && message.authorId === this.state.botUserId) {
622
+ return;
623
+ }
624
+ // Optionally ignore other bots
625
+ if (this.config.ignoreBots && message.isBot) {
626
+ return;
627
+ }
628
+ // Check if we should handle this channel
629
+ if (this.config.channelIds && !this.config.channelIds.includes(message.channelId)) {
630
+ return;
631
+ }
632
+ // Check if we should handle this guild
633
+ if (message.guildId &&
634
+ this.config.guildIds &&
635
+ !this.config.guildIds.includes(message.guildId)) {
636
+ return;
637
+ }
638
+ // Check if we should handle DMs
639
+ const isDM = !message.guildId;
640
+ if (isDM && !this.config.handleDMs) {
641
+ return;
642
+ }
643
+ // Determine if this message should trigger inference
644
+ const shouldTrigger = this.shouldTriggerInference(message, isDM);
645
+ // Convert message content including attachments
646
+ const contentBlocks = await this.convertMessageToContent(message);
647
+ // Only queue the event - all state writes happen in onProcess()
648
+ this.ctx.pushEvent({
649
+ type: 'discord:message',
650
+ discordMessageId: message.id,
651
+ channelId: message.channelId,
652
+ guildId: message.guildId,
653
+ authorId: message.authorId,
654
+ authorName: message.authorName,
655
+ content: message.content,
656
+ contentBlocks,
657
+ timestamp: message.timestamp.getTime(),
658
+ attachments: message.attachments,
659
+ shouldTriggerInference: shouldTrigger,
660
+ });
661
+ }
662
+ /**
663
+ * Convert Discord message to ContentBlock array.
664
+ * Includes text content and any supported attachments (images, text files).
665
+ */
666
+ async convertMessageToContent(message) {
667
+ const blocks = [];
668
+ // Add text content if present
669
+ if (message.content.trim()) {
670
+ blocks.push({ type: 'text', text: message.content });
671
+ }
672
+ // Process attachments
673
+ for (const attachment of message.attachments) {
674
+ const contentType = attachment.contentType?.toLowerCase() ?? '';
675
+ const filename = attachment.filename.toLowerCase();
676
+ // Check for supported image types
677
+ if (SUPPORTED_IMAGE_TYPES.some(type => contentType.startsWith(type))) {
678
+ blocks.push({
679
+ type: 'image',
680
+ source: {
681
+ type: 'url',
682
+ url: attachment.url,
683
+ },
684
+ });
685
+ continue;
686
+ }
687
+ // Check for text files - fetch and include content
688
+ const isTextFile = TEXT_FILE_EXTENSIONS.some(ext => filename.endsWith(ext));
689
+ if (isTextFile && attachment.size && attachment.size < 100_000) { // < 100KB
690
+ try {
691
+ const response = await fetch(attachment.url);
692
+ if (response.ok) {
693
+ const textContent = await response.text();
694
+ blocks.push({
695
+ type: 'text',
696
+ text: `\n--- File: ${attachment.filename} ---\n${textContent}\n--- End of ${attachment.filename} ---\n`,
697
+ });
698
+ }
699
+ }
700
+ catch (error) {
701
+ // If fetch fails, just note the attachment
702
+ blocks.push({
703
+ type: 'text',
704
+ text: `[Attachment: ${attachment.filename} (failed to fetch)]`,
705
+ });
706
+ }
707
+ continue;
708
+ }
709
+ // For unsupported attachments, add a note
710
+ blocks.push({
711
+ type: 'text',
712
+ text: `[Attachment: ${attachment.filename} (${contentType || 'unknown type'})]`,
713
+ });
714
+ }
715
+ // Ensure we have at least empty text if nothing else
716
+ if (blocks.length === 0) {
717
+ blocks.push({ type: 'text', text: '[Empty message]' });
718
+ }
719
+ return blocks;
720
+ }
721
+ /**
722
+ * Determine if a message should trigger agent inference based on config.
723
+ */
724
+ shouldTriggerInference(message, isDM) {
725
+ const triggerMode = this.config.triggerOn ?? 'mention_or_reply';
726
+ const botUserId = this.state.botUserId;
727
+ // Check if bot is mentioned
728
+ const isMentioned = botUserId
729
+ ? message.mentionedUserIds.includes(botUserId)
730
+ : false;
731
+ // Check if this is a reply to bot's message
732
+ const isReplyToBot = botUserId
733
+ ? message.replyToAuthorId === botUserId
734
+ : false;
735
+ switch (triggerMode) {
736
+ case 'all':
737
+ // Always trigger (use with caution!)
738
+ return true;
739
+ case 'mention':
740
+ // Only trigger on direct @mention
741
+ return isMentioned;
742
+ case 'mention_or_reply':
743
+ // Trigger on @mention OR when replying to bot
744
+ return isMentioned || isReplyToBot;
745
+ case 'dm_or_mention':
746
+ // DMs always trigger, channels require mention
747
+ return isDM || isMentioned;
748
+ default:
749
+ // Fallback: mention_or_reply behavior
750
+ return isMentioned || isReplyToBot;
751
+ }
752
+ }
753
+ handleDiscordMessageEdit(messageId, newContent) {
754
+ if (!this.ctx)
755
+ return;
756
+ // Only queue the event - state writes happen in onProcess()
757
+ this.ctx.pushEvent({
758
+ type: 'discord:edit',
759
+ discordMessageId: messageId,
760
+ newContent,
761
+ });
762
+ }
763
+ handleDiscordMessageDelete(messageId) {
764
+ if (!this.ctx)
765
+ return;
766
+ // Only queue the event - state writes happen in onProcess()
767
+ this.ctx.pushEvent({
768
+ type: 'discord:delete',
769
+ discordMessageId: messageId,
770
+ });
771
+ }
772
+ // ==========================================================================
773
+ // Tool Handlers
774
+ // ==========================================================================
775
+ async handleSend(input) {
776
+ if (!this.checkRateLimit(input.channelId)) {
777
+ return { success: false, error: 'Rate limited', isError: true };
778
+ }
779
+ const result = await this.client.sendMessage(input.channelId, input.content, {
780
+ replyTo: input.replyTo,
781
+ createThread: input.createThread,
782
+ });
783
+ // Track for thought-editing
784
+ this.lastSentMessage = { channelId: input.channelId, messageId: result.messageId };
785
+ return { success: true, data: { messageId: result.messageId } };
786
+ }
787
+ async handleDM(input) {
788
+ const result = await this.client.sendDM(input.userId, input.content);
789
+ return { success: true, data: { messageId: result.messageId } };
790
+ }
791
+ async handleReply(input) {
792
+ const replyCtx = this.replyContexts.get('default');
793
+ if (!replyCtx) {
794
+ return { success: false, error: 'No conversation context', isError: true };
795
+ }
796
+ if (!this.checkRateLimit(replyCtx.channelId)) {
797
+ return { success: false, error: 'Rate limited', isError: true };
798
+ }
799
+ let content = input.content;
800
+ if (input.mention && replyCtx.userId) {
801
+ content = `<@${replyCtx.userId}> ${content}`;
802
+ }
803
+ const targetChannel = replyCtx.threadId ?? replyCtx.channelId;
804
+ const result = await this.client.sendMessage(targetChannel, content, {
805
+ replyTo: replyCtx.replyToMessageId,
806
+ });
807
+ // Track for thought-editing
808
+ this.lastSentMessage = { channelId: targetChannel, messageId: result.messageId };
809
+ // Clear reply-to after sending
810
+ replyCtx.replyToMessageId = undefined;
811
+ replyCtx.lastActivity = Date.now();
812
+ return { success: true, data: { messageId: result.messageId } };
813
+ }
814
+ async handleReact(input) {
815
+ await this.client.addReaction(input.channelId, input.messageId, input.emoji);
816
+ return { success: true };
817
+ }
818
+ async handleEdit(input) {
819
+ await this.client.editMessage(input.channelId, input.messageId, input.content);
820
+ return { success: true };
821
+ }
822
+ async handleDelete(input) {
823
+ await this.client.deleteMessage(input.channelId, input.messageId);
824
+ return { success: true };
825
+ }
826
+ async handleCreateThread(input) {
827
+ const result = await this.client.createThread(input.channelId, input.messageId, input.name, input.autoArchiveDuration);
828
+ // Update reply context to use the new thread
829
+ const replyCtx = this.replyContexts.get('default');
830
+ if (replyCtx) {
831
+ replyCtx.threadId = result.threadId;
832
+ }
833
+ return { success: true, data: { threadId: result.threadId } };
834
+ }
835
+ async handleSetReplyContext(input) {
836
+ const replyCtx = this.replyContexts.get('default');
837
+ if (replyCtx) {
838
+ replyCtx.channelId = input.channelId;
839
+ replyCtx.threadId = input.threadId;
840
+ replyCtx.replyToMessageId = input.replyToMessageId;
841
+ }
842
+ else {
843
+ this.replyContexts.set('default', {
844
+ channelId: input.channelId,
845
+ threadId: input.threadId,
846
+ replyToMessageId: input.replyToMessageId,
847
+ userId: '',
848
+ guildId: null,
849
+ lastActivity: Date.now(),
850
+ });
851
+ }
852
+ return { success: true };
853
+ }
854
+ async handleActivate(input) {
855
+ if (!this.ctx) {
856
+ return { success: false, error: 'Module not started', isError: true };
857
+ }
858
+ this.ctx.registerSpeechHandler('*', { additive: input.additive });
859
+ return { success: true, data: { message: 'Discord is now handling speech' } };
860
+ }
861
+ async handleDeactivate() {
862
+ if (!this.ctx) {
863
+ return { success: false, error: 'Module not started', isError: true };
864
+ }
865
+ this.ctx.unregisterSpeechHandler();
866
+ return { success: true, data: { message: 'Discord is no longer handling speech' } };
867
+ }
868
+ async handleListGuilds() {
869
+ const guilds = await this.client.listGuilds();
870
+ return {
871
+ success: true,
872
+ data: {
873
+ guilds: guilds.map((g) => ({
874
+ id: g.id,
875
+ name: g.name,
876
+ memberCount: g.memberCount,
877
+ })),
878
+ },
879
+ };
880
+ }
881
+ async handleListChannels(input) {
882
+ const channels = await this.client.listChannels(input.guildId);
883
+ return {
884
+ success: true,
885
+ data: {
886
+ guildId: input.guildId,
887
+ channels: channels.map((c) => ({
888
+ id: c.id,
889
+ name: c.name,
890
+ type: c.type,
891
+ parentId: c.parentId,
892
+ })),
893
+ },
894
+ };
895
+ }
896
+ async handleFetchHistory(input) {
897
+ const limit = Math.min(input.limit ?? 50, 100); // Cap at 100
898
+ const messages = await this.client.fetchHistory(input.channelId, {
899
+ limit,
900
+ before: input.before,
901
+ });
902
+ // Update last read if we got messages
903
+ if (messages.length > 0) {
904
+ const latestId = messages[0].id; // Assuming sorted newest first
905
+ this.state.lastReadMessageId[input.channelId] = latestId;
906
+ this.ctx?.setState(this.state);
907
+ }
908
+ return {
909
+ success: true,
910
+ data: {
911
+ channelId: input.channelId,
912
+ messageCount: messages.length,
913
+ messages: messages.map((m) => ({
914
+ id: m.id,
915
+ author: m.authorName,
916
+ authorId: m.authorId,
917
+ isBot: m.isBot,
918
+ content: m.content,
919
+ timestamp: m.timestamp.toISOString(),
920
+ replyTo: m.replyTo,
921
+ })),
922
+ },
923
+ };
924
+ }
925
+ // ==========================================================================
926
+ // Helpers
927
+ // ==========================================================================
928
+ extractText(content) {
929
+ return content
930
+ .filter((block) => block.type === 'text')
931
+ .map((block) => block.text)
932
+ .join('\n');
933
+ }
934
+ checkRateLimit(channelId) {
935
+ const limit = this.config.rateLimitPerMinute ?? 30;
936
+ const now = Date.now();
937
+ const windowMs = 60 * 1000;
938
+ let info = this.state.rateLimits[channelId];
939
+ if (!info || now - info.windowStart > windowMs) {
940
+ // New window
941
+ info = { count: 0, windowStart: now };
942
+ this.state.rateLimits[channelId] = info;
943
+ }
944
+ if (info.count >= limit) {
945
+ return false;
946
+ }
947
+ info.count++;
948
+ return true;
949
+ }
950
+ cleanupOldConversations() {
951
+ const now = Date.now();
952
+ for (const [key, conv] of Object.entries(this.state.conversations)) {
953
+ if (now - conv.lastActivity > CONVERSATION_TIMEOUT_MS) {
954
+ delete this.state.conversations[key];
955
+ }
956
+ }
957
+ }
958
+ }
959
+ //# sourceMappingURL=index.js.map