@elizaos/plugin-signal 2.0.0-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1488 @@
1
+ // src/index.ts
2
+ import { logger } from "@elizaos/core";
3
+
4
+ // src/types.ts
5
+ var SignalEventTypes;
6
+ ((SignalEventTypes2) => {
7
+ SignalEventTypes2["MESSAGE_RECEIVED"] = "SIGNAL_MESSAGE_RECEIVED";
8
+ SignalEventTypes2["MESSAGE_SENT"] = "SIGNAL_MESSAGE_SENT";
9
+ SignalEventTypes2["REACTION_RECEIVED"] = "SIGNAL_REACTION_RECEIVED";
10
+ SignalEventTypes2["GROUP_JOINED"] = "SIGNAL_GROUP_JOINED";
11
+ SignalEventTypes2["GROUP_LEFT"] = "SIGNAL_GROUP_LEFT";
12
+ SignalEventTypes2["TYPING_STARTED"] = "SIGNAL_TYPING_STARTED";
13
+ SignalEventTypes2["TYPING_STOPPED"] = "SIGNAL_TYPING_STOPPED";
14
+ SignalEventTypes2["READ_RECEIPT"] = "SIGNAL_READ_RECEIPT";
15
+ })(SignalEventTypes ||= {});
16
+ var SIGNAL_SERVICE_NAME = "signal";
17
+ var ServiceType = {
18
+ SIGNAL: "signal"
19
+ };
20
+
21
+ class SignalPluginError extends Error {
22
+ code;
23
+ constructor(message, code) {
24
+ super(message);
25
+ this.code = code;
26
+ this.name = "SignalPluginError";
27
+ }
28
+ }
29
+
30
+ class SignalServiceNotInitializedError extends SignalPluginError {
31
+ constructor() {
32
+ super("Signal service is not initialized", "SERVICE_NOT_INITIALIZED");
33
+ this.name = "SignalServiceNotInitializedError";
34
+ }
35
+ }
36
+
37
+ class SignalClientNotAvailableError extends SignalPluginError {
38
+ constructor() {
39
+ super("Signal client is not available", "CLIENT_NOT_AVAILABLE");
40
+ this.name = "SignalClientNotAvailableError";
41
+ }
42
+ }
43
+
44
+ class SignalConfigurationError extends SignalPluginError {
45
+ constructor(missingConfig) {
46
+ super(`Missing required configuration: ${missingConfig}`, "MISSING_CONFIG");
47
+ this.name = "SignalConfigurationError";
48
+ }
49
+ }
50
+
51
+ class SignalApiError extends SignalPluginError {
52
+ apiErrorCode;
53
+ constructor(message, apiErrorCode) {
54
+ super(message, "API_ERROR");
55
+ this.apiErrorCode = apiErrorCode;
56
+ this.name = "SignalApiError";
57
+ }
58
+ }
59
+ function normalizeE164(number) {
60
+ let cleaned = number.replace(/[^\d+]/g, "");
61
+ if (!cleaned.startsWith("+")) {
62
+ if (cleaned.length === 10) {
63
+ cleaned = `+1${cleaned}`;
64
+ } else if (cleaned.length === 11 && cleaned.startsWith("1")) {
65
+ cleaned = `+${cleaned}`;
66
+ } else {
67
+ cleaned = `+${cleaned}`;
68
+ }
69
+ }
70
+ if (!/^\+\d{7,15}$/.test(cleaned)) {
71
+ return null;
72
+ }
73
+ return cleaned;
74
+ }
75
+ function isValidE164(number) {
76
+ return /^\+\d{7,15}$/.test(number);
77
+ }
78
+ function isValidGroupId(id) {
79
+ return /^[A-Za-z0-9+/]+=*$/.test(id) && id.length >= 32;
80
+ }
81
+ function isValidUuid(uuid) {
82
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(uuid);
83
+ }
84
+ function getSignalContactDisplayName(contact) {
85
+ return contact.profileName || contact.name || contact.number;
86
+ }
87
+ var MAX_SIGNAL_MESSAGE_LENGTH = 4000;
88
+ var MAX_SIGNAL_ATTACHMENT_SIZE = 100 * 1024 * 1024;
89
+
90
+ // src/actions/listContacts.ts
91
+ var listContacts = {
92
+ name: "SIGNAL_LIST_CONTACTS",
93
+ similes: [
94
+ "LIST_SIGNAL_CONTACTS",
95
+ "SHOW_CONTACTS",
96
+ "GET_CONTACTS",
97
+ "SIGNAL_CONTACTS"
98
+ ],
99
+ description: "List Signal contacts",
100
+ validate: async (_runtime, message, _state) => {
101
+ return message.content.source === "signal";
102
+ },
103
+ handler: async (runtime, message, _state, _options, callback) => {
104
+ const signalService = runtime.getService(SIGNAL_SERVICE_NAME);
105
+ if (!signalService || !signalService.isServiceConnected()) {
106
+ await callback?.({
107
+ text: "Signal service is not available.",
108
+ source: "signal"
109
+ });
110
+ return { success: false, error: "Signal service not available" };
111
+ }
112
+ const contacts = await signalService.getContacts();
113
+ const activeContacts = contacts.filter((c) => !c.blocked).sort((a, b) => {
114
+ const nameA = getSignalContactDisplayName(a);
115
+ const nameB = getSignalContactDisplayName(b);
116
+ return nameA.localeCompare(nameB);
117
+ });
118
+ const contactList = activeContacts.map((c) => {
119
+ const name = getSignalContactDisplayName(c);
120
+ const number = c.number;
121
+ return `• ${name} (${number})`;
122
+ });
123
+ const response = {
124
+ text: `Found ${activeContacts.length} contacts:
125
+
126
+ ${contactList.join(`
127
+ `)}`,
128
+ source: message.content.source
129
+ };
130
+ runtime.logger.debug({
131
+ src: "plugin:signal:action:list-contacts",
132
+ contactCount: activeContacts.length
133
+ }, "[SIGNAL_LIST_CONTACTS] Contacts listed");
134
+ await callback?.(response);
135
+ return {
136
+ success: true,
137
+ data: {
138
+ contactCount: activeContacts.length,
139
+ contacts: activeContacts.map((c) => ({
140
+ number: c.number,
141
+ name: getSignalContactDisplayName(c),
142
+ uuid: c.uuid
143
+ }))
144
+ }
145
+ };
146
+ },
147
+ examples: [
148
+ [
149
+ {
150
+ name: "{{user1}}",
151
+ content: {
152
+ text: "Show me my Signal contacts"
153
+ }
154
+ },
155
+ {
156
+ name: "{{agent}}",
157
+ content: {
158
+ text: "I'll list your Signal contacts.",
159
+ actions: ["SIGNAL_LIST_CONTACTS"]
160
+ }
161
+ }
162
+ ]
163
+ ]
164
+ };
165
+ var listContacts_default = listContacts;
166
+
167
+ // src/actions/listGroups.ts
168
+ var listGroups = {
169
+ name: "SIGNAL_LIST_GROUPS",
170
+ similes: ["LIST_SIGNAL_GROUPS", "SHOW_GROUPS", "GET_GROUPS", "SIGNAL_GROUPS"],
171
+ description: "List Signal groups",
172
+ validate: async (_runtime, message, _state) => {
173
+ return message.content.source === "signal";
174
+ },
175
+ handler: async (runtime, message, _state, _options, callback) => {
176
+ const signalService = runtime.getService(SIGNAL_SERVICE_NAME);
177
+ if (!signalService || !signalService.isServiceConnected()) {
178
+ await callback?.({
179
+ text: "Signal service is not available.",
180
+ source: "signal"
181
+ });
182
+ return { success: false, error: "Signal service not available" };
183
+ }
184
+ const groups = await signalService.getGroups();
185
+ const activeGroups = groups.filter((g) => g.isMember && !g.isBlocked).sort((a, b) => a.name.localeCompare(b.name));
186
+ const groupList = activeGroups.map((g) => {
187
+ const memberCount = g.members.length;
188
+ const description = g.description ? ` - ${g.description.slice(0, 50)}${g.description.length > 50 ? "..." : ""}` : "";
189
+ return `• ${g.name} (${memberCount} members)${description}`;
190
+ });
191
+ const response = {
192
+ text: `Found ${activeGroups.length} groups:
193
+
194
+ ${groupList.join(`
195
+ `)}`,
196
+ source: message.content.source
197
+ };
198
+ runtime.logger.debug({
199
+ src: "plugin:signal:action:list-groups",
200
+ groupCount: activeGroups.length
201
+ }, "[SIGNAL_LIST_GROUPS] Groups listed");
202
+ await callback?.(response);
203
+ return {
204
+ success: true,
205
+ data: {
206
+ groupCount: activeGroups.length,
207
+ groups: activeGroups.map((g) => ({
208
+ id: g.id,
209
+ name: g.name,
210
+ description: g.description,
211
+ memberCount: g.members.length
212
+ }))
213
+ }
214
+ };
215
+ },
216
+ examples: [
217
+ [
218
+ {
219
+ name: "{{user1}}",
220
+ content: {
221
+ text: "Show me my Signal groups"
222
+ }
223
+ },
224
+ {
225
+ name: "{{agent}}",
226
+ content: {
227
+ text: "I'll list your Signal groups.",
228
+ actions: ["SIGNAL_LIST_GROUPS"]
229
+ }
230
+ }
231
+ ]
232
+ ]
233
+ };
234
+ var listGroups_default = listGroups;
235
+
236
+ // src/actions/sendMessage.ts
237
+ import {
238
+ composePromptFromState,
239
+ ModelType,
240
+ parseJSONObjectFromText
241
+ } from "@elizaos/core";
242
+ var sendMessageTemplate = `You are helping to extract send message parameters for Signal.
243
+
244
+ The user wants to send a message to a Signal contact or group.
245
+
246
+ Recent conversation:
247
+ {{recentMessages}}
248
+
249
+ Extract the following:
250
+ 1. text: The message text to send
251
+ 2. recipient: The phone number (E.164 format like +1234567890) or group ID to send to (default: "current" for current conversation)
252
+
253
+ Respond with a JSON object like:
254
+ {
255
+ "text": "The message to send",
256
+ "recipient": "current"
257
+ }
258
+
259
+ Only respond with the JSON object, no other text.`;
260
+ var sendMessage = {
261
+ name: "SIGNAL_SEND_MESSAGE",
262
+ similes: [
263
+ "SEND_SIGNAL_MESSAGE",
264
+ "TEXT_SIGNAL",
265
+ "MESSAGE_SIGNAL",
266
+ "SIGNAL_TEXT"
267
+ ],
268
+ description: "Send a message to a Signal contact or group",
269
+ validate: async (_runtime, message, _state) => {
270
+ return message.content.source === "signal";
271
+ },
272
+ handler: async (runtime, message, state, _options, callback) => {
273
+ const signalService = runtime.getService(SIGNAL_SERVICE_NAME);
274
+ if (!signalService || !signalService.isServiceConnected()) {
275
+ await callback?.({
276
+ text: "Signal service is not available.",
277
+ source: "signal"
278
+ });
279
+ return { success: false, error: "Signal service not available" };
280
+ }
281
+ const composedState = state ?? {
282
+ values: {},
283
+ data: {},
284
+ text: ""
285
+ };
286
+ const prompt = composePromptFromState({
287
+ state: composedState,
288
+ template: sendMessageTemplate
289
+ });
290
+ let messageInfo = null;
291
+ for (let attempt = 0;attempt < 3; attempt++) {
292
+ const response2 = await runtime.useModel(ModelType.TEXT_SMALL, {
293
+ prompt
294
+ });
295
+ const parsedResponse = parseJSONObjectFromText(response2);
296
+ if (parsedResponse?.text) {
297
+ messageInfo = {
298
+ text: String(parsedResponse.text),
299
+ recipient: parsedResponse.recipient ? String(parsedResponse.recipient) : "current"
300
+ };
301
+ break;
302
+ }
303
+ }
304
+ if (!messageInfo || !messageInfo.text) {
305
+ await callback?.({
306
+ text: "I couldn't understand what message you want me to send. Please try again with a clearer request.",
307
+ source: "signal"
308
+ });
309
+ return { success: false, error: "Could not extract message parameters" };
310
+ }
311
+ const stateData = state?.data;
312
+ const room = stateData?.room || await runtime.getRoom(message.roomId);
313
+ if (!room) {
314
+ await callback?.({
315
+ text: "I couldn't determine the current conversation.",
316
+ source: "signal"
317
+ });
318
+ return { success: false, error: "Could not determine conversation" };
319
+ }
320
+ let targetRecipient = room.channelId || "";
321
+ const isGroup = room.metadata?.isGroup || false;
322
+ if (messageInfo.recipient && messageInfo.recipient !== "current") {
323
+ const normalized = normalizeE164(messageInfo.recipient);
324
+ if (normalized) {
325
+ targetRecipient = normalized;
326
+ } else if (isValidGroupId(messageInfo.recipient)) {
327
+ targetRecipient = messageInfo.recipient;
328
+ }
329
+ }
330
+ let result;
331
+ if (isGroup || isValidGroupId(targetRecipient)) {
332
+ result = await signalService.sendGroupMessage(targetRecipient, messageInfo.text);
333
+ } else {
334
+ result = await signalService.sendMessage(targetRecipient, messageInfo.text);
335
+ }
336
+ const response = {
337
+ text: "Message sent successfully.",
338
+ source: message.content.source
339
+ };
340
+ runtime.logger.debug({
341
+ src: "plugin:signal:action:send-message",
342
+ timestamp: result.timestamp,
343
+ recipient: targetRecipient
344
+ }, "[SIGNAL_SEND_MESSAGE] Message sent successfully");
345
+ await callback?.(response);
346
+ return {
347
+ success: true,
348
+ data: {
349
+ timestamp: result.timestamp,
350
+ recipient: targetRecipient
351
+ }
352
+ };
353
+ },
354
+ examples: [
355
+ [
356
+ {
357
+ name: "{{user1}}",
358
+ content: {
359
+ text: "Send a message to +1234567890 saying 'Hello!'"
360
+ }
361
+ },
362
+ {
363
+ name: "{{agent}}",
364
+ content: {
365
+ text: "I'll send that message for you.",
366
+ actions: ["SIGNAL_SEND_MESSAGE"]
367
+ }
368
+ }
369
+ ]
370
+ ]
371
+ };
372
+ var sendMessage_default = sendMessage;
373
+
374
+ // src/actions/sendReaction.ts
375
+ import {
376
+ composePromptFromState as composePromptFromState2,
377
+ ModelType as ModelType2,
378
+ parseJSONObjectFromText as parseJSONObjectFromText2
379
+ } from "@elizaos/core";
380
+ var sendReactionTemplate = `You are helping to extract reaction parameters for Signal.
381
+
382
+ The user wants to react to a Signal message with an emoji.
383
+
384
+ Recent conversation:
385
+ {{recentMessages}}
386
+
387
+ Extract the following:
388
+ 1. emoji: The emoji to react with (single emoji character)
389
+ 2. targetTimestamp: The timestamp of the message to react to (number)
390
+ 3. targetAuthor: The phone number of the message author
391
+ 4. remove: Whether to remove the reaction instead of adding it (default: false)
392
+
393
+ Respond with a JSON object like:
394
+ {
395
+ "emoji": "\uD83D\uDC4D",
396
+ "targetTimestamp": 1234567890000,
397
+ "targetAuthor": "+1234567890",
398
+ "remove": false
399
+ }
400
+
401
+ Only respond with the JSON object, no other text.`;
402
+ var sendReaction = {
403
+ name: "SIGNAL_SEND_REACTION",
404
+ similes: [
405
+ "REACT_SIGNAL",
406
+ "SIGNAL_REACT",
407
+ "ADD_SIGNAL_REACTION",
408
+ "SIGNAL_EMOJI"
409
+ ],
410
+ description: "React to a Signal message with an emoji",
411
+ validate: async (_runtime, message, _state) => {
412
+ return message.content.source === "signal";
413
+ },
414
+ handler: async (runtime, message, state, _options, callback) => {
415
+ const signalService = runtime.getService(SIGNAL_SERVICE_NAME);
416
+ if (!signalService || !signalService.isServiceConnected()) {
417
+ await callback?.({
418
+ text: "Signal service is not available.",
419
+ source: "signal"
420
+ });
421
+ return { success: false, error: "Signal service not available" };
422
+ }
423
+ const composedState = state ?? {
424
+ values: {},
425
+ data: {},
426
+ text: ""
427
+ };
428
+ const prompt = composePromptFromState2({
429
+ state: composedState,
430
+ template: sendReactionTemplate
431
+ });
432
+ let reactionInfo = null;
433
+ for (let attempt = 0;attempt < 3; attempt++) {
434
+ const response2 = await runtime.useModel(ModelType2.TEXT_SMALL, {
435
+ prompt
436
+ });
437
+ const parsedResponse = parseJSONObjectFromText2(response2);
438
+ if (parsedResponse?.emoji && parsedResponse?.targetTimestamp && parsedResponse?.targetAuthor) {
439
+ reactionInfo = {
440
+ emoji: String(parsedResponse.emoji),
441
+ targetTimestamp: Number(parsedResponse.targetTimestamp),
442
+ targetAuthor: String(parsedResponse.targetAuthor),
443
+ remove: Boolean(parsedResponse.remove)
444
+ };
445
+ break;
446
+ }
447
+ }
448
+ if (!reactionInfo) {
449
+ await callback?.({
450
+ text: "I couldn't understand the reaction request. Please specify the emoji and message to react to.",
451
+ source: "signal"
452
+ });
453
+ return { success: false, error: "Could not extract reaction parameters" };
454
+ }
455
+ const stateData = state?.data;
456
+ const room = stateData?.room || await runtime.getRoom(message.roomId);
457
+ const recipient = room?.channelId || reactionInfo.targetAuthor;
458
+ if (reactionInfo.remove) {
459
+ await signalService.removeReaction(recipient, reactionInfo.emoji, reactionInfo.targetTimestamp, reactionInfo.targetAuthor);
460
+ } else {
461
+ await signalService.sendReaction(recipient, reactionInfo.emoji, reactionInfo.targetTimestamp, reactionInfo.targetAuthor);
462
+ }
463
+ const actionWord = reactionInfo.remove ? "removed" : "added";
464
+ const response = {
465
+ text: `Reaction ${reactionInfo.emoji} ${actionWord} successfully.`,
466
+ source: message.content.source
467
+ };
468
+ await callback?.(response);
469
+ return {
470
+ success: true,
471
+ data: {
472
+ emoji: reactionInfo.emoji,
473
+ targetTimestamp: reactionInfo.targetTimestamp,
474
+ targetAuthor: reactionInfo.targetAuthor,
475
+ action: actionWord
476
+ }
477
+ };
478
+ },
479
+ examples: [
480
+ [
481
+ {
482
+ name: "{{user1}}",
483
+ content: {
484
+ text: "React to the last message with a thumbs up"
485
+ }
486
+ },
487
+ {
488
+ name: "{{agent}}",
489
+ content: {
490
+ text: "I'll add a thumbs up reaction.",
491
+ actions: ["SIGNAL_SEND_REACTION"]
492
+ }
493
+ }
494
+ ]
495
+ ]
496
+ };
497
+ var sendReaction_default = sendReaction;
498
+
499
+ // src/providers/conversationState.ts
500
+ var conversationStateProvider = {
501
+ name: "signalConversationState",
502
+ description: "Provides information about the current Signal conversation context",
503
+ get: async (runtime, message, state) => {
504
+ const room = state.data?.room ?? await runtime.getRoom(message.roomId);
505
+ if (!room) {
506
+ return {
507
+ data: {},
508
+ values: {},
509
+ text: ""
510
+ };
511
+ }
512
+ if (message.content.source !== "signal") {
513
+ return {
514
+ data: {},
515
+ values: {},
516
+ text: ""
517
+ };
518
+ }
519
+ const agentName = String(state?.agentName || "The agent");
520
+ const senderName = String(state?.senderName || "someone");
521
+ let responseText = "";
522
+ let conversationType = "";
523
+ let contactName = "";
524
+ let groupName = "";
525
+ const channelId = room.channelId ?? "";
526
+ const signalService = runtime.getService(ServiceType.SIGNAL);
527
+ if (!signalService || !signalService.isServiceConnected()) {
528
+ return {
529
+ data: {
530
+ room,
531
+ conversationType: "unknown",
532
+ channelId
533
+ },
534
+ values: {
535
+ conversationType: "unknown",
536
+ channelId
537
+ },
538
+ text: ""
539
+ };
540
+ }
541
+ const isGroup = room.metadata?.isGroup || false;
542
+ if (isGroup) {
543
+ conversationType = "GROUP";
544
+ const groupId = room.metadata?.groupId;
545
+ const group = signalService.getCachedGroup(groupId);
546
+ groupName = group?.name || room.name || "Unknown Group";
547
+ responseText = `${agentName} is currently in a Signal group chat: "${groupName}".`;
548
+ responseText += `
549
+ ${agentName} should be aware that multiple people can see this conversation and should participate when relevant.`;
550
+ if (group?.description) {
551
+ responseText += `
552
+ Group description: ${group.description}`;
553
+ }
554
+ } else {
555
+ conversationType = "DM";
556
+ const contact = signalService.getContact(channelId);
557
+ contactName = contact ? String(getSignalContactDisplayName(contact)) : senderName;
558
+ responseText = `${agentName} is currently in a direct message conversation with ${contactName} on Signal.`;
559
+ responseText += `
560
+ ${agentName} should engage naturally in conversation, responding to messages addressed to them.`;
561
+ }
562
+ responseText += `
563
+
564
+ Signal is an encrypted messaging platform, so all messages are secure and private.`;
565
+ return {
566
+ data: {
567
+ room,
568
+ conversationType,
569
+ contactName,
570
+ groupName,
571
+ channelId,
572
+ isGroup,
573
+ accountNumber: signalService.getAccountNumber()
574
+ },
575
+ values: {
576
+ conversationType,
577
+ contactName,
578
+ groupName,
579
+ channelId,
580
+ isGroup
581
+ },
582
+ text: responseText
583
+ };
584
+ }
585
+ };
586
+
587
+ // src/service.ts
588
+ import {
589
+ ChannelType,
590
+ createUniqueUuid,
591
+ Service,
592
+ stringToUuid
593
+ } from "@elizaos/core";
594
+ var getMessageService = (runtime) => {
595
+ if ("messageService" in runtime) {
596
+ const withMessageService = runtime;
597
+ return withMessageService.messageService ?? null;
598
+ }
599
+ return null;
600
+ };
601
+
602
+ class SignalApiClient {
603
+ baseUrl;
604
+ accountNumber;
605
+ constructor(baseUrl, accountNumber) {
606
+ this.baseUrl = baseUrl;
607
+ this.accountNumber = accountNumber;
608
+ }
609
+ async request(method, endpoint, body) {
610
+ const url = `${this.baseUrl}${endpoint}`;
611
+ const options = {
612
+ method,
613
+ headers: {
614
+ "Content-Type": "application/json"
615
+ }
616
+ };
617
+ if (body) {
618
+ options.body = JSON.stringify(body);
619
+ }
620
+ const response = await fetch(url, options);
621
+ if (!response.ok) {
622
+ const errorText = await response.text();
623
+ throw new Error(`Signal API error: ${response.status} - ${errorText}`);
624
+ }
625
+ const text = await response.text();
626
+ return text ? JSON.parse(text) : {};
627
+ }
628
+ async sendMessage(recipient, message, options) {
629
+ const body = {
630
+ message,
631
+ number: this.accountNumber,
632
+ recipients: [recipient]
633
+ };
634
+ if (options?.attachments) {
635
+ body.base64_attachments = options.attachments;
636
+ }
637
+ if (options?.quote) {
638
+ body.quote_timestamp = options.quote.timestamp;
639
+ body.quote_author = options.quote.author;
640
+ }
641
+ return this.request("POST", "/v2/send", body);
642
+ }
643
+ async sendGroupMessage(groupId, message, options) {
644
+ const body = {
645
+ message,
646
+ number: this.accountNumber,
647
+ recipients: [`group.${groupId}`]
648
+ };
649
+ if (options?.attachments) {
650
+ body.base64_attachments = options.attachments;
651
+ }
652
+ return this.request("POST", "/v2/send", body);
653
+ }
654
+ async sendReaction(recipient, emoji, targetTimestamp, targetAuthor, remove = false) {
655
+ await this.request("POST", `/v1/reactions/${this.accountNumber}`, {
656
+ recipient,
657
+ reaction: emoji,
658
+ target_author: targetAuthor,
659
+ timestamp: targetTimestamp,
660
+ remove
661
+ });
662
+ }
663
+ async getContacts() {
664
+ const result = await this.request("GET", `/v1/contacts/${this.accountNumber}`);
665
+ return result.contacts || [];
666
+ }
667
+ async getGroups() {
668
+ const result = await this.request("GET", `/v1/groups/${this.accountNumber}`);
669
+ return result || [];
670
+ }
671
+ async getGroup(groupId) {
672
+ const groups = await this.getGroups();
673
+ return groups.find((g) => g.id === groupId) || null;
674
+ }
675
+ async receive() {
676
+ const result = await this.request("GET", `/v1/receive/${this.accountNumber}`);
677
+ return result || [];
678
+ }
679
+ async sendTyping(recipient, stop = false) {
680
+ await this.request("PUT", `/v1/typing-indicator/${this.accountNumber}`, {
681
+ recipient,
682
+ stop
683
+ });
684
+ }
685
+ async setProfile(name, about) {
686
+ await this.request("PUT", `/v1/profiles/${this.accountNumber}`, {
687
+ name,
688
+ about: about || ""
689
+ });
690
+ }
691
+ async getIdentities() {
692
+ const result = await this.request("GET", `/v1/identities/${this.accountNumber}`);
693
+ return result || [];
694
+ }
695
+ async trustIdentity(number, trustLevel) {
696
+ await this.request("PUT", `/v1/identities/${this.accountNumber}/trust/${number}`, {
697
+ trust_level: trustLevel
698
+ });
699
+ }
700
+ }
701
+
702
+ class SignalService extends Service {
703
+ static serviceType = SIGNAL_SERVICE_NAME;
704
+ capabilityDescription = "The agent is able to send and receive messages on Signal";
705
+ async stop() {
706
+ await this.shutdown();
707
+ }
708
+ character;
709
+ accountNumber = null;
710
+ isConnected = false;
711
+ client = null;
712
+ settings;
713
+ contactCache = new Map;
714
+ groupCache = new Map;
715
+ pollInterval = null;
716
+ isPolling = false;
717
+ constructor(runtime) {
718
+ super(runtime);
719
+ if (runtime) {
720
+ this.character = runtime.character;
721
+ this.settings = this.loadSettings();
722
+ } else {
723
+ this.character = {};
724
+ this.settings = {
725
+ shouldIgnoreGroupMessages: false,
726
+ allowedGroups: undefined,
727
+ blockedNumbers: undefined
728
+ };
729
+ }
730
+ }
731
+ loadSettings() {
732
+ const ignoreGroups = this.runtime.getSetting("SIGNAL_SHOULD_IGNORE_GROUP_MESSAGES");
733
+ return {
734
+ shouldIgnoreGroupMessages: ignoreGroups === "true" || ignoreGroups === true,
735
+ allowedGroups: undefined,
736
+ blockedNumbers: undefined
737
+ };
738
+ }
739
+ static async start(runtime) {
740
+ const service = new SignalService(runtime);
741
+ const accountNumber = runtime.getSetting("SIGNAL_ACCOUNT_NUMBER");
742
+ const httpUrl = runtime.getSetting("SIGNAL_HTTP_URL");
743
+ if (!accountNumber) {
744
+ runtime.logger.warn({ src: "plugin:signal", agentId: runtime.agentId }, "SIGNAL_ACCOUNT_NUMBER not provided, Signal service will not start");
745
+ return service;
746
+ }
747
+ const normalizedNumber = normalizeE164(accountNumber);
748
+ if (!normalizedNumber) {
749
+ runtime.logger.error({ src: "plugin:signal", agentId: runtime.agentId, accountNumber }, "Invalid SIGNAL_ACCOUNT_NUMBER format");
750
+ return service;
751
+ }
752
+ service.accountNumber = normalizedNumber;
753
+ if (httpUrl) {
754
+ service.client = new SignalApiClient(httpUrl, normalizedNumber);
755
+ await service.initialize();
756
+ } else {
757
+ runtime.logger.warn({ src: "plugin:signal", agentId: runtime.agentId }, "SIGNAL_HTTP_URL not provided, Signal service will not be able to communicate");
758
+ }
759
+ return service;
760
+ }
761
+ static async stop(runtime) {
762
+ const service = runtime.getService(SIGNAL_SERVICE_NAME);
763
+ if (service) {
764
+ await service.shutdown();
765
+ }
766
+ }
767
+ async initialize() {
768
+ if (!this.client)
769
+ return;
770
+ this.runtime.logger.info({
771
+ src: "plugin:signal",
772
+ agentId: this.runtime.agentId,
773
+ accountNumber: this.accountNumber
774
+ }, "Initializing Signal service");
775
+ const contacts = await this.client.getContacts();
776
+ this.runtime.logger.info({
777
+ src: "plugin:signal",
778
+ agentId: this.runtime.agentId,
779
+ contactCount: contacts.length
780
+ }, "Signal service connected");
781
+ for (const contact of contacts) {
782
+ this.contactCache.set(contact.number, contact);
783
+ }
784
+ const groups = await this.client.getGroups();
785
+ for (const group of groups) {
786
+ this.groupCache.set(group.id, group);
787
+ }
788
+ this.isConnected = true;
789
+ this.startPolling();
790
+ }
791
+ async shutdown() {
792
+ this.stopPolling();
793
+ this.client = null;
794
+ this.isConnected = false;
795
+ this.runtime.logger.info({ src: "plugin:signal", agentId: this.runtime.agentId }, "Signal service stopped");
796
+ }
797
+ startPolling() {
798
+ if (this.pollInterval)
799
+ return;
800
+ this.pollInterval = setInterval(async () => {
801
+ await this.pollMessages();
802
+ }, 2000);
803
+ }
804
+ stopPolling() {
805
+ if (this.pollInterval) {
806
+ clearInterval(this.pollInterval);
807
+ this.pollInterval = null;
808
+ }
809
+ }
810
+ async pollMessages() {
811
+ if (!this.client || this.isPolling)
812
+ return;
813
+ this.isPolling = true;
814
+ const messages = await this.client.receive();
815
+ for (const msg of messages) {
816
+ await this.handleIncomingMessage(msg);
817
+ }
818
+ this.isPolling = false;
819
+ }
820
+ async handleIncomingMessage(msg) {
821
+ if (msg.reaction) {
822
+ await this.handleReaction(msg);
823
+ return;
824
+ }
825
+ if (!msg.message && msg.attachments.length === 0) {
826
+ return;
827
+ }
828
+ const isGroupMessage = Boolean(msg.groupId);
829
+ if (isGroupMessage && this.settings.shouldIgnoreGroupMessages) {
830
+ return;
831
+ }
832
+ const memory = await this.buildMemoryFromMessage(msg);
833
+ if (!memory)
834
+ return;
835
+ const room = await this.ensureRoomExists(msg.sender, msg.groupId);
836
+ await this.runtime.createMemory(memory, "messages");
837
+ await this.runtime.emitEvent("SIGNAL_MESSAGE_RECEIVED" /* MESSAGE_RECEIVED */, {
838
+ runtime: this.runtime,
839
+ source: "signal"
840
+ });
841
+ await this.processMessage(memory, room, msg.sender, msg.groupId);
842
+ }
843
+ async handleReaction(msg) {
844
+ if (!msg.reaction)
845
+ return;
846
+ await this.runtime.emitEvent("SIGNAL_REACTION_RECEIVED" /* REACTION_RECEIVED */, {
847
+ runtime: this.runtime,
848
+ source: "signal"
849
+ });
850
+ }
851
+ async processMessage(memory, room, sender, groupId) {
852
+ const callback = async (response) => {
853
+ if (groupId) {
854
+ await this.sendGroupMessage(groupId, response.text || "");
855
+ } else {
856
+ await this.sendMessage(sender, response.text || "");
857
+ }
858
+ const responseMemory = {
859
+ id: createUniqueUuid(this.runtime, `signal-response-${Date.now()}`),
860
+ agentId: this.runtime.agentId,
861
+ roomId: room.id,
862
+ entityId: this.runtime.agentId,
863
+ content: {
864
+ text: response.text || "",
865
+ source: "signal",
866
+ inReplyTo: memory.id
867
+ },
868
+ createdAt: Date.now()
869
+ };
870
+ await this.runtime.createMemory(responseMemory, "messages");
871
+ await this.runtime.emitEvent("SIGNAL_MESSAGE_SENT" /* MESSAGE_SENT */, {
872
+ runtime: this.runtime,
873
+ source: "signal"
874
+ });
875
+ return [responseMemory];
876
+ };
877
+ const messageService = getMessageService(this.runtime);
878
+ if (messageService) {
879
+ await messageService.handleMessage(this.runtime, memory, callback);
880
+ }
881
+ }
882
+ async buildMemoryFromMessage(msg) {
883
+ const roomId = await this.getRoomId(msg.sender, msg.groupId);
884
+ const entityId = this.getEntityId(msg.sender);
885
+ const contact = this.contactCache.get(msg.sender);
886
+ const displayName = contact ? getSignalContactDisplayName(contact) : msg.sender;
887
+ const media = msg.attachments.map((att) => ({
888
+ id: att.id,
889
+ url: `signal://attachment/${att.id}`,
890
+ title: att.filename || att.id,
891
+ source: "signal",
892
+ description: att.caption || att.filename,
893
+ contentType: att.contentType
894
+ }));
895
+ const memory = {
896
+ id: createUniqueUuid(this.runtime, `signal-${msg.timestamp}`),
897
+ agentId: this.runtime.agentId,
898
+ roomId,
899
+ entityId,
900
+ content: {
901
+ text: msg.message || "",
902
+ source: "signal",
903
+ name: displayName,
904
+ ...media.length > 0 ? { attachments: media } : {}
905
+ },
906
+ createdAt: msg.timestamp
907
+ };
908
+ return memory;
909
+ }
910
+ async getRoomId(sender, groupId) {
911
+ const roomKey = groupId || sender;
912
+ return createUniqueUuid(this.runtime, `signal-room-${roomKey}`);
913
+ }
914
+ getEntityId(number) {
915
+ return stringToUuid(`signal-user-${number}`);
916
+ }
917
+ async ensureRoomExists(sender, groupId) {
918
+ const roomId = await this.getRoomId(sender, groupId);
919
+ const existingRoom = await this.runtime.getRoom(roomId);
920
+ if (existingRoom)
921
+ return existingRoom;
922
+ const isGroup = Boolean(groupId);
923
+ const group = groupId ? this.groupCache.get(groupId) : null;
924
+ const contact = this.contactCache.get(sender);
925
+ const room = {
926
+ id: roomId,
927
+ name: isGroup ? group?.name || `Signal Group ${groupId}` : contact ? getSignalContactDisplayName(contact) : sender,
928
+ agentId: this.runtime.agentId,
929
+ source: "signal",
930
+ type: isGroup ? ChannelType.GROUP : ChannelType.DM,
931
+ channelId: groupId || sender,
932
+ metadata: {
933
+ isGroup,
934
+ groupId,
935
+ sender,
936
+ groupName: group?.name,
937
+ groupDescription: group?.description
938
+ }
939
+ };
940
+ await this.runtime.createRoom(room);
941
+ return room;
942
+ }
943
+ async sendMessage(recipient, text, options) {
944
+ if (!this.client) {
945
+ throw new Error("Signal client not initialized");
946
+ }
947
+ const normalizedRecipient = normalizeE164(recipient);
948
+ if (!normalizedRecipient) {
949
+ throw new Error(`Invalid recipient number: ${recipient}`);
950
+ }
951
+ const messages = this.splitMessage(text);
952
+ let lastTimestamp = 0;
953
+ for (const msg of messages) {
954
+ const result = await this.client.sendMessage(normalizedRecipient, msg, options);
955
+ lastTimestamp = result.timestamp;
956
+ }
957
+ return { timestamp: lastTimestamp };
958
+ }
959
+ async sendGroupMessage(groupId, text, options) {
960
+ if (!this.client) {
961
+ throw new Error("Signal client not initialized");
962
+ }
963
+ const messages = this.splitMessage(text);
964
+ let lastTimestamp = 0;
965
+ for (const msg of messages) {
966
+ const result = await this.client.sendGroupMessage(groupId, msg, options);
967
+ lastTimestamp = result.timestamp;
968
+ }
969
+ return { timestamp: lastTimestamp };
970
+ }
971
+ async sendReaction(recipient, emoji, targetTimestamp, targetAuthor) {
972
+ if (!this.client) {
973
+ throw new Error("Signal client not initialized");
974
+ }
975
+ await this.client.sendReaction(recipient, emoji, targetTimestamp, targetAuthor);
976
+ }
977
+ async removeReaction(recipient, emoji, targetTimestamp, targetAuthor) {
978
+ if (!this.client) {
979
+ throw new Error("Signal client not initialized");
980
+ }
981
+ await this.client.sendReaction(recipient, emoji, targetTimestamp, targetAuthor, true);
982
+ }
983
+ async getContacts() {
984
+ if (!this.client) {
985
+ throw new Error("Signal client not initialized");
986
+ }
987
+ const contacts = await this.client.getContacts();
988
+ for (const contact of contacts) {
989
+ this.contactCache.set(contact.number, contact);
990
+ }
991
+ return contacts;
992
+ }
993
+ async getGroups() {
994
+ if (!this.client) {
995
+ throw new Error("Signal client not initialized");
996
+ }
997
+ const groups = await this.client.getGroups();
998
+ for (const group of groups) {
999
+ this.groupCache.set(group.id, group);
1000
+ }
1001
+ return groups;
1002
+ }
1003
+ async getGroup(groupId) {
1004
+ if (!this.client) {
1005
+ throw new Error("Signal client not initialized");
1006
+ }
1007
+ const group = await this.client.getGroup(groupId);
1008
+ if (group) {
1009
+ this.groupCache.set(group.id, group);
1010
+ }
1011
+ return group;
1012
+ }
1013
+ async sendTypingIndicator(recipient) {
1014
+ if (!this.client)
1015
+ return;
1016
+ await this.client.sendTyping(recipient);
1017
+ }
1018
+ async stopTypingIndicator(recipient) {
1019
+ if (!this.client)
1020
+ return;
1021
+ await this.client.sendTyping(recipient, true);
1022
+ }
1023
+ splitMessage(text) {
1024
+ if (text.length <= MAX_SIGNAL_MESSAGE_LENGTH) {
1025
+ return [text];
1026
+ }
1027
+ const messages = [];
1028
+ let remaining = text;
1029
+ while (remaining.length > 0) {
1030
+ if (remaining.length <= MAX_SIGNAL_MESSAGE_LENGTH) {
1031
+ messages.push(remaining);
1032
+ break;
1033
+ }
1034
+ let splitIndex = MAX_SIGNAL_MESSAGE_LENGTH;
1035
+ const lastNewline = remaining.lastIndexOf(`
1036
+ `, MAX_SIGNAL_MESSAGE_LENGTH);
1037
+ if (lastNewline > MAX_SIGNAL_MESSAGE_LENGTH / 2) {
1038
+ splitIndex = lastNewline + 1;
1039
+ } else {
1040
+ const lastSpace = remaining.lastIndexOf(" ", MAX_SIGNAL_MESSAGE_LENGTH);
1041
+ if (lastSpace > MAX_SIGNAL_MESSAGE_LENGTH / 2) {
1042
+ splitIndex = lastSpace + 1;
1043
+ }
1044
+ }
1045
+ messages.push(remaining.slice(0, splitIndex));
1046
+ remaining = remaining.slice(splitIndex);
1047
+ }
1048
+ return messages;
1049
+ }
1050
+ getContact(number) {
1051
+ return this.contactCache.get(number) || null;
1052
+ }
1053
+ getCachedGroup(groupId) {
1054
+ return this.groupCache.get(groupId) || null;
1055
+ }
1056
+ getAccountNumber() {
1057
+ return this.accountNumber;
1058
+ }
1059
+ isServiceConnected() {
1060
+ return this.isConnected;
1061
+ }
1062
+ }
1063
+
1064
+ // src/accounts.ts
1065
+ var DEFAULT_ACCOUNT_ID = "default";
1066
+ function normalizeAccountId(accountId) {
1067
+ if (!accountId || typeof accountId !== "string") {
1068
+ return DEFAULT_ACCOUNT_ID;
1069
+ }
1070
+ const trimmed = accountId.trim().toLowerCase();
1071
+ return trimmed || DEFAULT_ACCOUNT_ID;
1072
+ }
1073
+ function getMultiAccountConfig(runtime) {
1074
+ const characterSignal = runtime.character?.settings?.signal;
1075
+ return {
1076
+ enabled: characterSignal?.enabled,
1077
+ account: characterSignal?.account,
1078
+ httpUrl: characterSignal?.httpUrl,
1079
+ accounts: characterSignal?.accounts
1080
+ };
1081
+ }
1082
+ function listSignalAccountIds(runtime) {
1083
+ const config = getMultiAccountConfig(runtime);
1084
+ const accounts = config.accounts;
1085
+ if (!accounts || typeof accounts !== "object") {
1086
+ return [DEFAULT_ACCOUNT_ID];
1087
+ }
1088
+ const ids = Object.keys(accounts).filter(Boolean);
1089
+ if (ids.length === 0) {
1090
+ return [DEFAULT_ACCOUNT_ID];
1091
+ }
1092
+ return ids.toSorted((a, b) => a.localeCompare(b));
1093
+ }
1094
+ function resolveDefaultSignalAccountId(runtime) {
1095
+ const ids = listSignalAccountIds(runtime);
1096
+ if (ids.includes(DEFAULT_ACCOUNT_ID)) {
1097
+ return DEFAULT_ACCOUNT_ID;
1098
+ }
1099
+ return ids[0] ?? DEFAULT_ACCOUNT_ID;
1100
+ }
1101
+ function getAccountConfig(runtime, accountId) {
1102
+ const config = getMultiAccountConfig(runtime);
1103
+ const accounts = config.accounts;
1104
+ if (!accounts || typeof accounts !== "object") {
1105
+ return;
1106
+ }
1107
+ return accounts[accountId];
1108
+ }
1109
+ function filterDefined(obj) {
1110
+ return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined));
1111
+ }
1112
+ function mergeSignalAccountConfig(runtime, accountId) {
1113
+ const multiConfig = getMultiAccountConfig(runtime);
1114
+ const { accounts: _ignored, ...baseConfig } = multiConfig;
1115
+ const accountConfig = getAccountConfig(runtime, accountId) ?? {};
1116
+ const envAccount = runtime.getSetting("SIGNAL_ACCOUNT_NUMBER");
1117
+ const envHttpUrl = runtime.getSetting("SIGNAL_HTTP_URL");
1118
+ const envCliPath = runtime.getSetting("SIGNAL_CLI_PATH");
1119
+ const envIgnoreGroups = runtime.getSetting("SIGNAL_SHOULD_IGNORE_GROUP_MESSAGES");
1120
+ const envConfig = {
1121
+ account: envAccount || undefined,
1122
+ httpUrl: envHttpUrl || undefined,
1123
+ cliPath: envCliPath || undefined,
1124
+ shouldIgnoreGroupMessages: envIgnoreGroups?.toLowerCase() === "true"
1125
+ };
1126
+ return {
1127
+ ...filterDefined(envConfig),
1128
+ ...filterDefined(baseConfig),
1129
+ ...filterDefined(accountConfig)
1130
+ };
1131
+ }
1132
+ function resolveBaseUrl(config) {
1133
+ if (config.httpUrl?.trim()) {
1134
+ return config.httpUrl.trim().replace(/\/+$/, "");
1135
+ }
1136
+ const host = config.httpHost?.trim() || "127.0.0.1";
1137
+ const port = config.httpPort ?? 8080;
1138
+ return `http://${host}:${port}`;
1139
+ }
1140
+ function resolveSignalAccount(runtime, accountId) {
1141
+ const normalizedAccountId = normalizeAccountId(accountId);
1142
+ const multiConfig = getMultiAccountConfig(runtime);
1143
+ const baseEnabled = multiConfig.enabled !== false;
1144
+ const merged = mergeSignalAccountConfig(runtime, normalizedAccountId);
1145
+ const accountEnabled = merged.enabled !== false;
1146
+ const enabled = baseEnabled && accountEnabled;
1147
+ const baseUrl = resolveBaseUrl(merged);
1148
+ const configured = Boolean(merged.account?.trim() || merged.httpUrl?.trim() || merged.cliPath?.trim() || merged.httpHost?.trim() || typeof merged.httpPort === "number" || typeof merged.autoStart === "boolean");
1149
+ return {
1150
+ accountId: normalizedAccountId,
1151
+ enabled,
1152
+ name: merged.name?.trim() || undefined,
1153
+ account: merged.account?.trim(),
1154
+ baseUrl,
1155
+ configured,
1156
+ config: merged
1157
+ };
1158
+ }
1159
+ function listEnabledSignalAccounts(runtime) {
1160
+ return listSignalAccountIds(runtime).map((accountId) => resolveSignalAccount(runtime, accountId)).filter((account) => account.enabled && account.configured);
1161
+ }
1162
+ function isMultiAccountEnabled(runtime) {
1163
+ const accounts = listEnabledSignalAccounts(runtime);
1164
+ return accounts.length > 1;
1165
+ }
1166
+ // src/rpc.ts
1167
+ var DEFAULT_TIMEOUT_MS = 1e4;
1168
+ function normalizeBaseUrl(url) {
1169
+ const trimmed = url.trim();
1170
+ if (!trimmed) {
1171
+ throw new Error("Signal base URL is required");
1172
+ }
1173
+ if (/^https?:\/\//i.test(trimmed)) {
1174
+ return trimmed.replace(/\/+$/, "");
1175
+ }
1176
+ return `http://${trimmed}`.replace(/\/+$/, "");
1177
+ }
1178
+ function generateId() {
1179
+ return crypto.randomUUID();
1180
+ }
1181
+ async function fetchWithTimeout(url, init, timeoutMs) {
1182
+ const controller = new AbortController;
1183
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1184
+ try {
1185
+ return await fetch(url, { ...init, signal: controller.signal });
1186
+ } finally {
1187
+ clearTimeout(timer);
1188
+ }
1189
+ }
1190
+ async function signalRpcRequest(method, params, opts) {
1191
+ const baseUrl = normalizeBaseUrl(opts.baseUrl);
1192
+ const id = generateId();
1193
+ const body = JSON.stringify({
1194
+ jsonrpc: "2.0",
1195
+ method,
1196
+ params,
1197
+ id
1198
+ });
1199
+ const res = await fetchWithTimeout(`${baseUrl}/api/v1/rpc`, {
1200
+ method: "POST",
1201
+ headers: { "Content-Type": "application/json" },
1202
+ body
1203
+ }, opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
1204
+ if (res.status === 201) {
1205
+ return;
1206
+ }
1207
+ const text = await res.text();
1208
+ if (!text) {
1209
+ throw new Error(`Signal RPC empty response (status ${res.status})`);
1210
+ }
1211
+ const parsed = JSON.parse(text);
1212
+ if (parsed.error) {
1213
+ const code = parsed.error.code ?? "unknown";
1214
+ const msg = parsed.error.message ?? "Signal RPC error";
1215
+ throw new Error(`Signal RPC ${code}: ${msg}`);
1216
+ }
1217
+ return parsed.result;
1218
+ }
1219
+ async function signalCheck(baseUrl, timeoutMs = DEFAULT_TIMEOUT_MS) {
1220
+ const normalized = normalizeBaseUrl(baseUrl);
1221
+ try {
1222
+ const res = await fetchWithTimeout(`${normalized}/api/v1/check`, { method: "GET" }, timeoutMs);
1223
+ if (!res.ok) {
1224
+ return { ok: false, status: res.status, error: `HTTP ${res.status}` };
1225
+ }
1226
+ return { ok: true, status: res.status, error: null };
1227
+ } catch (err) {
1228
+ return {
1229
+ ok: false,
1230
+ status: null,
1231
+ error: err instanceof Error ? err.message : String(err)
1232
+ };
1233
+ }
1234
+ }
1235
+ async function signalGetVersion(opts) {
1236
+ return signalRpcRequest("version", undefined, opts);
1237
+ }
1238
+ async function signalListAccounts(opts) {
1239
+ return signalRpcRequest("listAccounts", undefined, opts);
1240
+ }
1241
+ async function signalListContacts(account, opts) {
1242
+ return signalRpcRequest("listContacts", { account }, opts);
1243
+ }
1244
+ async function signalListGroups(account, opts) {
1245
+ return signalRpcRequest("listGroups", { account }, opts);
1246
+ }
1247
+ async function signalSend(params, opts) {
1248
+ return signalRpcRequest("send", params, opts);
1249
+ }
1250
+ async function signalSendReaction(params, opts) {
1251
+ return signalRpcRequest("sendReaction", params, opts);
1252
+ }
1253
+ async function signalSendTyping(params, opts) {
1254
+ return signalRpcRequest("sendTyping", params, opts);
1255
+ }
1256
+ async function signalSendReadReceipt(params, opts) {
1257
+ return signalRpcRequest("sendReadReceipt", params, opts);
1258
+ }
1259
+ async function streamSignalEvents(params) {
1260
+ const baseUrl = normalizeBaseUrl(params.baseUrl);
1261
+ const url = new URL(`${baseUrl}/api/v1/events`);
1262
+ if (params.account) {
1263
+ url.searchParams.set("account", params.account);
1264
+ }
1265
+ const res = await fetch(url, {
1266
+ method: "GET",
1267
+ headers: { Accept: "text/event-stream" },
1268
+ signal: params.abortSignal
1269
+ });
1270
+ if (!res.ok || !res.body) {
1271
+ throw new Error(`Signal SSE failed (${res.status} ${res.statusText || "error"})`);
1272
+ }
1273
+ const reader = res.body.getReader();
1274
+ const decoder = new TextDecoder;
1275
+ let buffer = "";
1276
+ let currentEvent = {};
1277
+ const flushEvent = () => {
1278
+ if (!currentEvent.data && !currentEvent.event && !currentEvent.id) {
1279
+ return;
1280
+ }
1281
+ params.onEvent({
1282
+ event: currentEvent.event,
1283
+ data: currentEvent.data,
1284
+ id: currentEvent.id
1285
+ });
1286
+ currentEvent = {};
1287
+ };
1288
+ while (true) {
1289
+ const { value, done } = await reader.read();
1290
+ if (done) {
1291
+ break;
1292
+ }
1293
+ buffer += decoder.decode(value, { stream: true });
1294
+ let lineEnd = buffer.indexOf(`
1295
+ `);
1296
+ while (lineEnd !== -1) {
1297
+ let line = buffer.slice(0, lineEnd);
1298
+ buffer = buffer.slice(lineEnd + 1);
1299
+ if (line.endsWith("\r")) {
1300
+ line = line.slice(0, -1);
1301
+ }
1302
+ if (line === "") {
1303
+ flushEvent();
1304
+ lineEnd = buffer.indexOf(`
1305
+ `);
1306
+ continue;
1307
+ }
1308
+ if (line.startsWith(":")) {
1309
+ lineEnd = buffer.indexOf(`
1310
+ `);
1311
+ continue;
1312
+ }
1313
+ const colonIndex = line.indexOf(":");
1314
+ if (colonIndex === -1) {
1315
+ lineEnd = buffer.indexOf(`
1316
+ `);
1317
+ continue;
1318
+ }
1319
+ const field = line.slice(0, colonIndex).trim();
1320
+ let value2 = line.slice(colonIndex + 1);
1321
+ if (value2.startsWith(" ")) {
1322
+ value2 = value2.slice(1);
1323
+ }
1324
+ if (field === "event") {
1325
+ currentEvent.event = value2;
1326
+ } else if (field === "data") {
1327
+ currentEvent.data = currentEvent.data ? `${currentEvent.data}
1328
+ ${value2}` : value2;
1329
+ } else if (field === "id") {
1330
+ currentEvent.id = value2;
1331
+ }
1332
+ lineEnd = buffer.indexOf(`
1333
+ `);
1334
+ }
1335
+ }
1336
+ flushEvent();
1337
+ }
1338
+ function parseSignalEventData(data) {
1339
+ if (!data) {
1340
+ return null;
1341
+ }
1342
+ try {
1343
+ return JSON.parse(data);
1344
+ } catch {
1345
+ return null;
1346
+ }
1347
+ }
1348
+ function createSignalEventStream(params) {
1349
+ let abortController = null;
1350
+ let running = false;
1351
+ let reconnectDelay = params.reconnectDelayMs ?? 1000;
1352
+ const maxDelay = params.maxReconnectDelayMs ?? 30000;
1353
+ const connect = async () => {
1354
+ if (!running) {
1355
+ return;
1356
+ }
1357
+ abortController = new AbortController;
1358
+ try {
1359
+ params.onConnect?.();
1360
+ reconnectDelay = params.reconnectDelayMs ?? 1000;
1361
+ await streamSignalEvents({
1362
+ baseUrl: params.baseUrl,
1363
+ account: params.account,
1364
+ abortSignal: abortController.signal,
1365
+ onEvent: params.onEvent
1366
+ });
1367
+ } catch (error) {
1368
+ if (error instanceof Error && error.name === "AbortError") {
1369
+ return;
1370
+ }
1371
+ params.onError?.(error instanceof Error ? error : new Error(String(error)));
1372
+ } finally {
1373
+ params.onDisconnect?.();
1374
+ }
1375
+ if (running) {
1376
+ setTimeout(connect, reconnectDelay);
1377
+ reconnectDelay = Math.min(reconnectDelay * 2, maxDelay);
1378
+ }
1379
+ };
1380
+ return {
1381
+ start: () => {
1382
+ if (running) {
1383
+ return;
1384
+ }
1385
+ running = true;
1386
+ connect();
1387
+ },
1388
+ stop: () => {
1389
+ running = false;
1390
+ abortController?.abort();
1391
+ abortController = null;
1392
+ },
1393
+ isRunning: () => running
1394
+ };
1395
+ }
1396
+
1397
+ // src/index.ts
1398
+ var signalPlugin = {
1399
+ name: "signal",
1400
+ description: "Signal messaging integration plugin for ElizaOS with end-to-end encryption",
1401
+ services: [SignalService],
1402
+ actions: [sendMessage_default, sendReaction_default, listContacts_default, listGroups_default],
1403
+ providers: [conversationStateProvider],
1404
+ init: async (_config, runtime) => {
1405
+ const accountNumber = runtime.getSetting("SIGNAL_ACCOUNT_NUMBER");
1406
+ const httpUrl = runtime.getSetting("SIGNAL_HTTP_URL");
1407
+ const cliPath = runtime.getSetting("SIGNAL_CLI_PATH");
1408
+ const ignoreGroups = runtime.getSetting("SIGNAL_SHOULD_IGNORE_GROUP_MESSAGES");
1409
+ const maskNumber = (number) => {
1410
+ if (!number || number.trim() === "")
1411
+ return "[not set]";
1412
+ if (number.length <= 6)
1413
+ return "***";
1414
+ return `${number.slice(0, 3)}...${number.slice(-2)}`;
1415
+ };
1416
+ logger.info({
1417
+ src: "plugin:signal",
1418
+ agentId: runtime.agentId,
1419
+ settings: {
1420
+ accountNumber: maskNumber(accountNumber),
1421
+ httpUrl: httpUrl || "[not set]",
1422
+ cliPath: cliPath || "[not set]",
1423
+ ignoreGroups: ignoreGroups || "false"
1424
+ }
1425
+ }, "Signal plugin initializing");
1426
+ if (!accountNumber || accountNumber.trim() === "") {
1427
+ logger.warn({ src: "plugin:signal", agentId: runtime.agentId }, "SIGNAL_ACCOUNT_NUMBER not provided - Signal plugin is loaded but will not be functional");
1428
+ return;
1429
+ }
1430
+ const normalizedNumber = normalizeE164(accountNumber);
1431
+ if (!normalizedNumber) {
1432
+ logger.error({ src: "plugin:signal", agentId: runtime.agentId, accountNumber }, "SIGNAL_ACCOUNT_NUMBER is not a valid E.164 phone number");
1433
+ return;
1434
+ }
1435
+ if (!httpUrl && !cliPath) {
1436
+ logger.warn({ src: "plugin:signal", agentId: runtime.agentId }, "Neither SIGNAL_HTTP_URL nor SIGNAL_CLI_PATH provided - Signal plugin will not be able to communicate");
1437
+ return;
1438
+ }
1439
+ logger.info({ src: "plugin:signal", agentId: runtime.agentId }, "Signal plugin configuration validated successfully");
1440
+ }
1441
+ };
1442
+ var src_default = signalPlugin;
1443
+ export {
1444
+ streamSignalEvents,
1445
+ signalSendTyping,
1446
+ signalSendReadReceipt,
1447
+ signalSendReaction,
1448
+ signalSend,
1449
+ signalRpcRequest,
1450
+ signalListGroups,
1451
+ signalListContacts,
1452
+ signalListAccounts,
1453
+ signalGetVersion,
1454
+ signalCheck,
1455
+ sendReaction,
1456
+ sendMessage,
1457
+ resolveSignalAccount,
1458
+ resolveDefaultSignalAccountId,
1459
+ parseSignalEventData,
1460
+ normalizeE164,
1461
+ normalizeBaseUrl,
1462
+ normalizeAccountId,
1463
+ listSignalAccountIds,
1464
+ listGroups,
1465
+ listEnabledSignalAccounts,
1466
+ listContacts,
1467
+ isValidUuid,
1468
+ isValidGroupId,
1469
+ isValidE164,
1470
+ isMultiAccountEnabled,
1471
+ getSignalContactDisplayName,
1472
+ src_default as default,
1473
+ createSignalEventStream,
1474
+ conversationStateProvider,
1475
+ SignalServiceNotInitializedError,
1476
+ SignalService,
1477
+ SignalPluginError,
1478
+ SignalEventTypes,
1479
+ SignalConfigurationError,
1480
+ SignalClientNotAvailableError,
1481
+ SignalApiError,
1482
+ SIGNAL_SERVICE_NAME,
1483
+ MAX_SIGNAL_MESSAGE_LENGTH,
1484
+ MAX_SIGNAL_ATTACHMENT_SIZE,
1485
+ DEFAULT_ACCOUNT_ID
1486
+ };
1487
+
1488
+ //# debugId=35D8D7AB25F728BD64756E2164756E21