@elizaos/plugin-signal 2.0.0-beta.1 → 2.0.3-beta.3

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 CHANGED
@@ -4334,7 +4334,7 @@ function normalizeAccountId(accountId) {
4334
4334
  return trimmed || DEFAULT_ACCOUNT_ID;
4335
4335
  }
4336
4336
  function getMultiAccountConfig(runtime) {
4337
- const characterSignal = runtime.character?.settings?.signal;
4337
+ const characterSignal = runtime.character.settings?.signal;
4338
4338
  return {
4339
4339
  enabled: characterSignal?.enabled,
4340
4340
  account: characterSignal?.account,
@@ -4436,7 +4436,7 @@ function purposeForAccount(_account) {
4436
4436
  return ["messaging"];
4437
4437
  }
4438
4438
  function accessGateForAccount(account) {
4439
- const dmPolicy = account.config?.dm?.policy;
4439
+ const dmPolicy = account.config.dm?.policy;
4440
4440
  if (dmPolicy === "disabled")
4441
4441
  return "disabled";
4442
4442
  if (dmPolicy === "pairing")
@@ -4463,8 +4463,8 @@ function toConnectorAccount(account) {
4463
4463
  metadata: {
4464
4464
  phoneNumber: account.account,
4465
4465
  baseUrl: account.baseUrl,
4466
- dmPolicy: account.config?.dm?.policy ?? "pairing",
4467
- groupPolicy: account.config?.group?.policy ?? "open"
4466
+ dmPolicy: account.config.dm?.policy ?? "pairing",
4467
+ groupPolicy: account.config.group?.policy ?? "open"
4468
4468
  }
4469
4469
  };
4470
4470
  }
@@ -4506,6 +4506,7 @@ import {
4506
4506
  ChannelType,
4507
4507
  createMessageMemory,
4508
4508
  createUniqueUuid,
4509
+ EventType,
4509
4510
  lifeOpsPassiveConnectorsEnabled,
4510
4511
  Service,
4511
4512
  stringToUuid
@@ -4963,7 +4964,7 @@ class SignalPairingSession {
4963
4964
  let qrCode;
4964
4965
  try {
4965
4966
  const importedQrCode = await Promise.resolve().then(() => __toESM(require_server(), 1));
4966
- qrCode = importedQrCode.default ?? importedQrCode;
4967
+ qrCode = importedQrCode.default;
4967
4968
  } catch (err) {
4968
4969
  const message = `Failed to load QR dependency: ${String(err)}`;
4969
4970
  this.lastError = message;
@@ -5217,11 +5218,9 @@ var SignalEventTypes;
5217
5218
  SignalEventTypes2["REACTION_RECEIVED"] = "SIGNAL_REACTION_RECEIVED";
5218
5219
  SignalEventTypes2["GROUP_JOINED"] = "SIGNAL_GROUP_JOINED";
5219
5220
  SignalEventTypes2["GROUP_LEFT"] = "SIGNAL_GROUP_LEFT";
5220
- SignalEventTypes2["TYPING_STARTED"] = "SIGNAL_TYPING_STARTED";
5221
- SignalEventTypes2["TYPING_STOPPED"] = "SIGNAL_TYPING_STOPPED";
5222
- SignalEventTypes2["READ_RECEIPT"] = "SIGNAL_READ_RECEIPT";
5223
5221
  })(SignalEventTypes ||= {});
5224
5222
  var SIGNAL_SERVICE_NAME = "signal";
5223
+
5225
5224
  class SignalPluginError extends Error {
5226
5225
  code;
5227
5226
  constructor(message, code) {
@@ -5592,70 +5591,80 @@ class SignalService extends Service {
5592
5591
  accountScopedKey(accountId, kind, value) {
5593
5592
  return accountId === DEFAULT_ACCOUNT_ID ? `signal-${kind}-${value}` : `signal-${kind}-${accountId}-${value}`;
5594
5593
  }
5595
- static async start(runtime) {
5596
- const service = new SignalService(runtime);
5597
- const accounts = listEnabledSignalAccounts(runtime);
5594
+ static resolveStartupConfig(runtime) {
5598
5595
  const rawAuthDir = runtime.getSetting("SIGNAL_AUTH_DIR");
5599
5596
  const defaultAuthDir = typeof rawAuthDir === "string" && rawAuthDir.trim().length > 0 ? rawAuthDir.trim() : defaultSignalAuthDir();
5600
5597
  const configuredCliPath = runtime.getSetting("SIGNAL_CLI_PATH") || DEFAULT_SIGNAL_CLI_PATH;
5601
5598
  const parsedStartupTimeout = Number.parseInt(String(runtime.getSetting("SIGNAL_STARTUP_TIMEOUT_MS") ?? ""), 10);
5602
5599
  const startupTimeoutMs = Number.isFinite(parsedStartupTimeout) && parsedStartupTimeout > 0 ? Math.min(parsedStartupTimeout, 120000) : DEFAULT_SIGNAL_DAEMON_STARTUP_TIMEOUT_MS;
5600
+ return { defaultAuthDir, configuredCliPath, startupTimeoutMs };
5601
+ }
5602
+ async initializeConfiguredAccount(account, config) {
5603
+ const accountNumber = account.account;
5604
+ if (!accountNumber) {
5605
+ this.runtime.logger.warn({ src: "plugin:signal", agentId: this.runtime.agentId, accountId: account.accountId }, "Signal account is missing account number, skipping");
5606
+ return;
5607
+ }
5608
+ const normalizedNumber = normalizeE164(accountNumber);
5609
+ if (!normalizedNumber) {
5610
+ this.runtime.logger.error({
5611
+ src: "plugin:signal",
5612
+ agentId: this.runtime.agentId,
5613
+ accountId: account.accountId,
5614
+ accountNumber
5615
+ }, "Invalid Signal account number format");
5616
+ return;
5617
+ }
5618
+ const baseUrl = normalizeBaseUrl2(account.baseUrl);
5619
+ const authDir = account.config.authDir?.trim() || config.defaultAuthDir;
5620
+ const accountCliPath = account.config.cliPath?.trim() || config.configuredCliPath;
5621
+ if (!account.config.httpUrl?.trim() && !await this.ensureAccountDaemon(account, accountCliPath, authDir, baseUrl, config)) {
5622
+ return;
5623
+ }
5624
+ const client = new SignalApiClient(baseUrl, normalizedNumber);
5625
+ this.clients.set(account.accountId, client);
5626
+ this.accountNumbers.set(account.accountId, normalizedNumber);
5627
+ if (account.accountId === this.defaultAccountId || !this.client) {
5628
+ this.client = client;
5629
+ this.accountNumber = normalizedNumber;
5630
+ }
5631
+ }
5632
+ async ensureAccountDaemon(account, cliPath, authDir, baseUrl, config) {
5633
+ if (!fs2.existsSync(authDir)) {
5634
+ this.runtime.logger.warn({
5635
+ src: "plugin:signal",
5636
+ agentId: this.runtime.agentId,
5637
+ accountId: account.accountId,
5638
+ authDir
5639
+ }, "Signal auth directory does not exist yet — run `signal-cli -a <number> link` (or set SIGNAL_AUTH_DIR to a pre-existing install) before starting the plugin");
5640
+ return false;
5641
+ }
5642
+ try {
5643
+ await this.ensureDaemonRunning(cliPath, authDir, baseUrl, config.startupTimeoutMs);
5644
+ return true;
5645
+ } catch (error) {
5646
+ this.runtime.logger.error({
5647
+ src: "plugin:signal",
5648
+ agentId: this.runtime.agentId,
5649
+ accountId: account.accountId,
5650
+ error: String(error),
5651
+ authDir,
5652
+ cliPath
5653
+ }, "Failed to start signal-cli daemon");
5654
+ return false;
5655
+ }
5656
+ }
5657
+ static async start(runtime) {
5658
+ const service = new SignalService(runtime);
5659
+ const accounts = listEnabledSignalAccounts(runtime);
5660
+ const startupConfig = SignalService.resolveStartupConfig(runtime);
5603
5661
  if (accounts.length === 0) {
5604
5662
  runtime.logger.warn({ src: "plugin:signal", agentId: runtime.agentId }, "SIGNAL_ACCOUNT_NUMBER not provided, Signal service will not start");
5605
5663
  return service;
5606
5664
  }
5607
5665
  service.defaultAccountId = resolveDefaultSignalAccountId(runtime);
5608
5666
  for (const account of accounts) {
5609
- const accountNumber = account.account;
5610
- if (!accountNumber) {
5611
- runtime.logger.warn({ src: "plugin:signal", agentId: runtime.agentId, accountId: account.accountId }, "Signal account is missing account number, skipping");
5612
- continue;
5613
- }
5614
- const normalizedNumber = normalizeE164(accountNumber);
5615
- if (!normalizedNumber) {
5616
- runtime.logger.error({
5617
- src: "plugin:signal",
5618
- agentId: runtime.agentId,
5619
- accountId: account.accountId,
5620
- accountNumber
5621
- }, "Invalid Signal account number format");
5622
- continue;
5623
- }
5624
- const baseUrl = normalizeBaseUrl2(account.baseUrl);
5625
- const explicitHttpUrl = Boolean(account.config.httpUrl?.trim());
5626
- const authDir = account.config.authDir?.trim() || defaultAuthDir;
5627
- const accountCliPath = account.config.cliPath?.trim() || configuredCliPath;
5628
- if (!explicitHttpUrl) {
5629
- if (!fs2.existsSync(authDir)) {
5630
- runtime.logger.warn({
5631
- src: "plugin:signal",
5632
- agentId: runtime.agentId,
5633
- accountId: account.accountId,
5634
- authDir
5635
- }, "Signal auth directory does not exist yet — run `signal-cli -a <number> link` (or set SIGNAL_AUTH_DIR to a pre-existing install) before starting the plugin");
5636
- continue;
5637
- }
5638
- try {
5639
- await service.ensureDaemonRunning(accountCliPath, authDir, baseUrl, startupTimeoutMs);
5640
- } catch (error) {
5641
- runtime.logger.error({
5642
- src: "plugin:signal",
5643
- agentId: runtime.agentId,
5644
- accountId: account.accountId,
5645
- error: String(error),
5646
- authDir,
5647
- cliPath: accountCliPath
5648
- }, "Failed to start signal-cli daemon");
5649
- continue;
5650
- }
5651
- }
5652
- const client = new SignalApiClient(baseUrl, normalizedNumber);
5653
- service.clients.set(account.accountId, client);
5654
- service.accountNumbers.set(account.accountId, normalizedNumber);
5655
- if (account.accountId === service.defaultAccountId || !service.client) {
5656
- service.client = client;
5657
- service.accountNumber = normalizedNumber;
5658
- }
5667
+ await service.initializeConfiguredAccount(account, startupConfig);
5659
5668
  }
5660
5669
  if (service.clients.size === 0) {
5661
5670
  runtime.logger.warn({ src: "plugin:signal", agentId: runtime.agentId }, "No configured Signal accounts could be initialized");
@@ -5794,7 +5803,10 @@ class SignalService extends Service {
5794
5803
  }
5795
5804
  });
5796
5805
  },
5797
- getUser: service.getConnectorUser.bind(service),
5806
+ getUser: (handlerRuntime, params) => service.getConnectorUser(handlerRuntime, {
5807
+ ...params,
5808
+ target: params.target ? params.target.accountId ? params.target : { ...params.target, accountId: connectorAccountId } : { source: SIGNAL_SERVICE_NAME, accountId: connectorAccountId }
5809
+ }),
5798
5810
  getChatContext: async (target, context) => {
5799
5811
  const targetAccountId = service.normalizeAccountId(target.accountId ?? context.accountId ?? connectorAccountId);
5800
5812
  const room = target.roomId ? await context.runtime.getRoom(target.roomId) : null;
@@ -5802,7 +5814,8 @@ class SignalService extends Service {
5802
5814
  if (!channelId) {
5803
5815
  return null;
5804
5816
  }
5805
- const recentMessages = (await service.getRecentMessages(50, targetAccountId).catch(() => [])).filter((recent) => recent.channelId === channelId || recent.roomId === target.roomId).slice(0, 10).map((recent) => ({
5817
+ const signalRecentMessages = await service.getRecentMessages(50, targetAccountId).catch(() => []);
5818
+ const recentMessages = signalRecentMessages.filter((recent) => recent.channelId === channelId || recent.roomId === target.roomId).slice(0, 10).map((recent) => ({
5806
5819
  name: recent.speakerName,
5807
5820
  text: recent.text,
5808
5821
  timestamp: recent.createdAt,
@@ -6014,39 +6027,47 @@ class SignalService extends Service {
6014
6027
  }
6015
6028
  static unwrapEnvelope(raw) {
6016
6029
  if (!("envelope" in raw)) {
6017
- if (typeof raw.sender !== "string" || typeof raw.timestamp !== "number") {
6018
- return null;
6019
- }
6020
- return {
6021
- sender: raw.sender,
6022
- senderUuid: typeof raw.senderUuid === "string" ? raw.senderUuid : undefined,
6023
- message: typeof raw.message === "string" ? raw.message : undefined,
6024
- timestamp: raw.timestamp,
6025
- groupId: typeof raw.groupId === "string" ? raw.groupId : undefined,
6026
- attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
6027
- reaction: raw.reaction,
6028
- expiresInSeconds: typeof raw.expiresInSeconds === "number" ? raw.expiresInSeconds : undefined,
6029
- viewOnce: raw.viewOnce === true,
6030
- quote: raw.quote
6031
- };
6030
+ return SignalService.unwrapFlatMessage(raw);
6032
6031
  }
6033
- const env = raw.envelope;
6034
- const dm = env.dataMessage || {};
6035
- const groupInfo = dm.groupInfo;
6036
- const sender = env.sourceNumber || env.source;
6037
- const timestamp = dm.timestamp || env.timestamp;
6032
+ if (!isRecord(raw.envelope)) {
6033
+ return null;
6034
+ }
6035
+ return SignalService.unwrapEnvelopeMessage(raw.envelope);
6036
+ }
6037
+ static unwrapFlatMessage(raw) {
6038
+ if (typeof raw.sender !== "string" || typeof raw.timestamp !== "number") {
6039
+ return null;
6040
+ }
6041
+ return {
6042
+ sender: raw.sender,
6043
+ senderUuid: typeof raw.senderUuid === "string" ? raw.senderUuid : undefined,
6044
+ message: typeof raw.message === "string" ? raw.message : undefined,
6045
+ timestamp: raw.timestamp,
6046
+ groupId: typeof raw.groupId === "string" ? raw.groupId : undefined,
6047
+ attachments: Array.isArray(raw.attachments) ? raw.attachments : [],
6048
+ reaction: raw.reaction,
6049
+ expiresInSeconds: typeof raw.expiresInSeconds === "number" ? raw.expiresInSeconds : undefined,
6050
+ viewOnce: raw.viewOnce === true,
6051
+ quote: raw.quote
6052
+ };
6053
+ }
6054
+ static unwrapEnvelopeMessage(env) {
6055
+ const dm = isRecord(env.dataMessage) ? env.dataMessage : {};
6056
+ const groupInfo = isRecord(dm.groupInfo) ? dm.groupInfo : undefined;
6057
+ const sender = typeof env.sourceNumber === "string" && env.sourceNumber || typeof env.source === "string" && env.source || "";
6058
+ const timestamp = typeof dm.timestamp === "number" && dm.timestamp || typeof env.timestamp === "number" && env.timestamp || 0;
6038
6059
  if (!sender || !timestamp)
6039
6060
  return null;
6040
6061
  return {
6041
6062
  sender,
6042
- senderUuid: env.source,
6043
- message: dm.message,
6063
+ senderUuid: typeof env.source === "string" ? env.source : undefined,
6064
+ message: typeof dm.message === "string" ? dm.message : undefined,
6044
6065
  timestamp,
6045
- groupId: groupInfo?.groupId,
6046
- attachments: dm.attachments || [],
6066
+ groupId: typeof groupInfo?.groupId === "string" ? groupInfo.groupId : undefined,
6067
+ attachments: Array.isArray(dm.attachments) ? dm.attachments : [],
6047
6068
  reaction: dm.reaction,
6048
- expiresInSeconds: dm.expiresInSeconds || 0,
6049
- viewOnce: dm.viewOnce || false,
6069
+ expiresInSeconds: typeof dm.expiresInSeconds === "number" ? dm.expiresInSeconds : 0,
6070
+ viewOnce: dm.viewOnce === true,
6050
6071
  quote: dm.quote
6051
6072
  };
6052
6073
  }
@@ -6159,9 +6180,16 @@ class SignalService extends Service {
6159
6180
  this.runtime.logger.warn({ src: "plugin:signal", accountId: normalizedAccountId, roomId, sender: msg.sender }, "Room not found after ensureConnection, creating via ensureRoomExists");
6160
6181
  room = await this.ensureRoomExists(msg.sender, msg.groupId, normalizedAccountId);
6161
6182
  }
6162
- if (this.settings.autoReply && !lifeOpsPassiveConnectorsEnabled(this.runtime)) {
6183
+ const autoReply = this.settings.autoReply && !lifeOpsPassiveConnectorsEnabled(this.runtime);
6184
+ if (autoReply) {
6163
6185
  await this.processMessage(memory, room, msg.sender, msg.groupId, normalizedAccountId);
6186
+ return;
6164
6187
  }
6188
+ await this.runtime.emitEvent(EventType.MESSAGE_RECEIVED, {
6189
+ runtime: this.runtime,
6190
+ message: memory,
6191
+ source: "signal"
6192
+ });
6165
6193
  }
6166
6194
  async handleReaction(msg, accountId = this.defaultAccountId) {
6167
6195
  if (!msg.reaction)
@@ -6386,7 +6414,7 @@ class SignalService extends Service {
6386
6414
  const accountId = this.normalizeAccountId(args.accountId);
6387
6415
  const roomId = await this.getRoomId(args.isGroup ? this.getAccountNumberForAccount(accountId) || "signal-agent" : args.channelId, args.isGroup ? args.channelId : undefined, accountId);
6388
6416
  const worldId = createUniqueUuid(this.runtime, accountId === DEFAULT_ACCOUNT_ID ? "signal-world" : `signal-world-${accountId}`);
6389
- const displayName = this.character?.name || "Agent";
6417
+ const displayName = this.character.name || "Agent";
6390
6418
  await this.runtime.ensureConnection({
6391
6419
  entityId: this.runtime.agentId,
6392
6420
  roomId,
@@ -6514,11 +6542,11 @@ class SignalService extends Service {
6514
6542
  roomIds,
6515
6543
  limit: requestedLimit * 4
6516
6544
  });
6517
- return memories.filter((memory) => memory.content?.source === "signal").filter((memory) => this.memoryMatchesAccount(memory, normalizedAccountId)).filter((memory) => typeof memory.content?.text === "string" && memory.content.text.trim().length > 0).sort((left, right) => Number(right.createdAt ?? 0) - Number(left.createdAt ?? 0)).slice(0, requestedLimit).map((memory) => {
6545
+ return memories.filter((memory) => memory.content.source === "signal").filter((memory) => this.memoryMatchesAccount(memory, normalizedAccountId)).filter((memory) => typeof memory.content.text === "string" && memory.content.text.trim().length > 0).sort((left, right) => Number(right.createdAt ?? 0) - Number(left.createdAt ?? 0)).slice(0, requestedLimit).map((memory) => {
6518
6546
  const room = roomsById.get(memory.roomId);
6519
6547
  const isGroup = room?.type === ChannelType.GROUP || Boolean(room?.metadata?.isGroup);
6520
6548
  const text = String(memory.content.text ?? "").trim();
6521
- const speakerName = memory.entityId === this.runtime.agentId ? this.character?.name || "Agent" : typeof memory.content.name === "string" && memory.content.name.trim().length > 0 ? memory.content.name.trim() : room?.name || room?.channelId || "Unknown";
6549
+ const speakerName = memory.entityId === this.runtime.agentId ? this.character.name || "Agent" : typeof memory.content.name === "string" && memory.content.name.trim().length > 0 ? memory.content.name.trim() : room?.name || room?.channelId || "Unknown";
6522
6550
  return {
6523
6551
  id: String(memory.id),
6524
6552
  roomId: String(memory.roomId),
@@ -6578,7 +6606,7 @@ class SignalService extends Service {
6578
6606
  });
6579
6607
  const before = params.before ? Number(params.before) : undefined;
6580
6608
  const after = params.after ? Number(params.after) : undefined;
6581
- return memories.filter((memory) => memory.content?.source === "signal").filter((memory) => this.memoryMatchesAccount(memory, accountId)).filter((memory) => {
6609
+ return memories.filter((memory) => memory.content.source === "signal").filter((memory) => this.memoryMatchesAccount(memory, accountId)).filter((memory) => {
6582
6610
  const createdAt = Number(memory.createdAt ?? 0);
6583
6611
  if (before !== undefined && Number.isFinite(before) && createdAt >= before) {
6584
6612
  return false;
@@ -6599,12 +6627,23 @@ class SignalService extends Service {
6599
6627
  limit: Math.max(params.limit ?? 100, 100)
6600
6628
  });
6601
6629
  return memories.filter((memory) => {
6602
- const text = String(memory.content?.text ?? "").toLowerCase();
6603
- const name = String(memory.content?.name ?? "").toLowerCase();
6630
+ const text = String(memory.content.text ?? "").toLowerCase();
6631
+ const name = String(memory.content.name ?? "").toLowerCase();
6604
6632
  return text.includes(query) || name.includes(query);
6605
6633
  }).slice(0, params.limit ?? 25);
6606
6634
  }
6607
6635
  async reactConnectorMessage(runtime, params) {
6636
+ const reactionTarget = await this.resolveReactionTarget(runtime, params);
6637
+ if (!params.emoji) {
6638
+ throw new Error("Signal reaction requires emoji, targetTimestamp, and targetAuthor.");
6639
+ }
6640
+ if (params.remove) {
6641
+ await this.removeReaction(reactionTarget.recipient, params.emoji, reactionTarget.targetTimestamp, reactionTarget.targetAuthor, reactionTarget.accountId);
6642
+ return;
6643
+ }
6644
+ await this.sendReaction(reactionTarget.recipient, params.emoji, reactionTarget.targetTimestamp, reactionTarget.targetAuthor, reactionTarget.accountId);
6645
+ }
6646
+ async resolveReactionTarget(runtime, params) {
6608
6647
  const target = params.target;
6609
6648
  const accountId = this.normalizeAccountId(target?.accountId);
6610
6649
  const room = params.roomId || target?.roomId ? await runtime.getRoom(params.roomId ?? target?.roomId) : null;
@@ -6612,23 +6651,21 @@ class SignalService extends Service {
6612
6651
  if (!recipient) {
6613
6652
  throw new Error("Signal reaction requires a target recipient or room.");
6614
6653
  }
6615
- let targetTimestamp = params.targetTimestamp;
6616
- let targetAuthor = params.targetAuthor;
6617
- if ((!targetTimestamp || !targetAuthor) && params.messageId) {
6618
- const memory = await runtime.getMemoryById(params.messageId).catch(() => null);
6619
- const metadata = memory?.metadata;
6620
- const sender = metadata?.sender;
6621
- targetTimestamp = targetTimestamp ?? Number(metadata?.messageIdFull ?? metadata?.timestamp ?? memory?.createdAt);
6622
- targetAuthor = targetAuthor ?? (typeof sender?.id === "string" ? sender.id : typeof metadata?.fromId === "string" ? metadata.fromId : undefined);
6623
- }
6624
- if (!params.emoji || !targetTimestamp || !targetAuthor) {
6654
+ const fallback = params.messageId ? await this.lookupReactionTargetFromMemory(runtime, params.messageId) : {};
6655
+ const targetTimestamp = params.targetTimestamp ?? fallback.targetTimestamp;
6656
+ const targetAuthor = params.targetAuthor ?? fallback.targetAuthor;
6657
+ if (!targetTimestamp || !targetAuthor) {
6625
6658
  throw new Error("Signal reaction requires emoji, targetTimestamp, and targetAuthor.");
6626
6659
  }
6627
- if (params.remove) {
6628
- await this.removeReaction(recipient, params.emoji, targetTimestamp, targetAuthor, accountId);
6629
- return;
6630
- }
6631
- await this.sendReaction(recipient, params.emoji, targetTimestamp, targetAuthor, accountId);
6660
+ return { recipient, accountId, targetTimestamp, targetAuthor };
6661
+ }
6662
+ async lookupReactionTargetFromMemory(runtime, messageId) {
6663
+ const memory = await runtime.getMemoryById(messageId).catch(() => null);
6664
+ const metadata = memory?.metadata;
6665
+ const sender = metadata?.sender;
6666
+ const targetTimestamp = Number(metadata?.messageIdFull ?? metadata?.timestamp ?? memory?.createdAt);
6667
+ const targetAuthor = typeof sender?.id === "string" ? sender.id : typeof metadata?.fromId === "string" ? metadata.fromId : undefined;
6668
+ return { targetTimestamp, targetAuthor };
6632
6669
  }
6633
6670
  async getConnectorUser(_runtime, params) {
6634
6671
  const lookup = params.userId ?? params.handle ?? params.username ?? params.query;
@@ -6726,6 +6763,9 @@ class SignalService extends Service {
6726
6763
 
6727
6764
  // src/setup-routes.ts
6728
6765
  import path3 from "node:path";
6766
+ function setupError(code, message) {
6767
+ return { error: { code, message } };
6768
+ }
6729
6769
  var signalPairingSessions = new Map;
6730
6770
  var signalPairingSnapshots = new Map;
6731
6771
  var MAX_PAIRING_SESSIONS = 10;
@@ -6746,17 +6786,22 @@ function getSetupService(runtime) {
6746
6786
  const service = runtime.getService("connector-setup");
6747
6787
  return isConnectorSetupService(service) ? service : null;
6748
6788
  }
6749
- function resolveSignalStatusResponse(accountId, session, previousSnapshot, authExists, serviceConnected) {
6789
+ function buildSignalStatusResponse(accountId, session, previousSnapshot, authExists, serviceConnected) {
6750
6790
  const snapshot = session?.getSnapshot() ?? previousSnapshot;
6751
- const status = snapshot?.status ?? (authExists || serviceConnected ? "connected" : "idle");
6791
+ const pairingStatus = snapshot?.status ?? (authExists || serviceConnected ? "connected" : "idle");
6792
+ const state = pairingStatus === "connected" ? "paired" : pairingStatus === "error" || pairingStatus === "timeout" ? "error" : pairingStatus === "idle" || pairingStatus === "disconnected" ? "idle" : "configuring";
6752
6793
  return {
6753
- accountId,
6754
- status,
6755
- authExists,
6756
- serviceConnected,
6757
- qrDataUrl: snapshot?.qrDataUrl ?? null,
6758
- phoneNumber: snapshot?.phoneNumber ?? null,
6759
- error: snapshot?.error ?? null
6794
+ connector: "signal",
6795
+ state,
6796
+ detail: {
6797
+ accountId,
6798
+ pairingStatus,
6799
+ authExists,
6800
+ serviceConnected,
6801
+ qrDataUrl: snapshot?.qrDataUrl ?? null,
6802
+ phoneNumber: snapshot?.phoneNumber ?? null,
6803
+ pairingError: snapshot?.error ?? null
6804
+ }
6760
6805
  };
6761
6806
  }
6762
6807
  function reapTerminalSessions() {
@@ -6769,21 +6814,47 @@ function reapTerminalSessions() {
6769
6814
  }
6770
6815
  }
6771
6816
  }
6772
- async function handlePair(req, res, runtime) {
6817
+ function resolveServiceConnected(runtime) {
6818
+ const sigService = runtime.getService("signal");
6819
+ if (!sigService)
6820
+ return false;
6821
+ return Boolean(sigService.connected) || Boolean(sigService.isConnected) || typeof sigService.isServiceConnected === "function" && Boolean(sigService.isServiceConnected());
6822
+ }
6823
+ function extractAccountId(value) {
6824
+ return sanitizeAccountId(typeof value === "string" && value.trim() ? value.trim() : "default");
6825
+ }
6826
+ async function handleStatus(req, res, runtime) {
6827
+ reapTerminalSessions();
6828
+ const rawUrl = typeof req.url === "string" ? req.url : "/";
6829
+ const url = new URL(rawUrl, "http://localhost");
6830
+ let accountId;
6831
+ try {
6832
+ accountId = extractAccountId(url.searchParams.get("accountId"));
6833
+ } catch (err) {
6834
+ res.status(400).json(setupError("bad_request", err.message));
6835
+ return;
6836
+ }
6837
+ const setupService = getSetupService(runtime);
6838
+ const workspaceDir = setupService?.getWorkspaceDir() ?? "";
6839
+ const session = signalPairingSessions.get(accountId);
6840
+ const previousSnapshot = signalPairingSnapshots.get(accountId);
6841
+ const authExists = signalAuthExists(workspaceDir, accountId);
6842
+ const serviceConnected = resolveServiceConnected(runtime);
6843
+ res.status(200).json(buildSignalStatusResponse(accountId, session, previousSnapshot, authExists, serviceConnected));
6844
+ }
6845
+ async function handleStart(req, res, runtime) {
6773
6846
  reapTerminalSessions();
6774
6847
  const body = req.body ?? {};
6775
6848
  let accountId;
6776
6849
  try {
6777
- accountId = sanitizeAccountId(typeof body.accountId === "string" && body.accountId.trim() ? body.accountId.trim() : "default");
6850
+ accountId = extractAccountId(body.accountId);
6778
6851
  } catch (err) {
6779
- res.status(400).json({ error: err.message });
6852
+ res.status(400).json(setupError("bad_request", err.message));
6780
6853
  return;
6781
6854
  }
6782
6855
  const isReplacing = signalPairingSessions.has(accountId);
6783
6856
  if (!isReplacing && signalPairingSessions.size >= MAX_PAIRING_SESSIONS) {
6784
- res.status(429).json({
6785
- error: `Too many concurrent pairing sessions (max ${MAX_PAIRING_SESSIONS})`
6786
- });
6857
+ res.status(429).json(setupError("too_many_sessions", `Too many concurrent pairing sessions (max ${MAX_PAIRING_SESSIONS})`));
6787
6858
  return;
6788
6859
  }
6789
6860
  const setupService = getSetupService(runtime);
@@ -6852,60 +6923,15 @@ async function handlePair(req, res, runtime) {
6852
6923
  signalPairingSnapshots.set(accountId, session.getSnapshot());
6853
6924
  signalPairingSessions.delete(accountId);
6854
6925
  });
6855
- res.status(200).json({
6856
- ok: true,
6857
- ...resolveSignalStatusResponse(accountId, session, signalPairingSnapshots.get(accountId), false, false)
6858
- });
6926
+ res.status(200).json(buildSignalStatusResponse(accountId, session, signalPairingSnapshots.get(accountId), false, false));
6859
6927
  }
6860
- async function handleStatus(req, res, runtime) {
6861
- reapTerminalSessions();
6862
- const rawUrl = typeof req.url === "string" ? req.url : "/";
6863
- const url = new URL(rawUrl, "http://localhost");
6864
- let accountId;
6865
- try {
6866
- accountId = sanitizeAccountId(url.searchParams.get("accountId") || "default");
6867
- } catch (err) {
6868
- res.status(400).json({ error: err.message });
6869
- return;
6870
- }
6871
- const setupService = getSetupService(runtime);
6872
- const workspaceDir = setupService?.getWorkspaceDir() ?? "";
6873
- const session = signalPairingSessions.get(accountId);
6874
- const previousSnapshot = signalPairingSnapshots.get(accountId);
6875
- const authExists = signalAuthExists(workspaceDir, accountId);
6876
- let serviceConnected = false;
6877
- try {
6878
- const sigService = runtime.getService("signal");
6879
- if (sigService) {
6880
- serviceConnected = Boolean(sigService.connected) || Boolean(sigService.isConnected) || typeof sigService.isServiceConnected === "function" && Boolean(sigService.isServiceConnected());
6881
- }
6882
- } catch {}
6883
- res.status(200).json(resolveSignalStatusResponse(accountId, session, previousSnapshot, authExists, serviceConnected));
6884
- }
6885
- async function handlePairStop(req, res, _runtime) {
6928
+ async function handleCancel(req, res, runtime) {
6886
6929
  const body = req.body ?? {};
6887
6930
  let accountId;
6888
6931
  try {
6889
- accountId = sanitizeAccountId(typeof body.accountId === "string" && body.accountId.trim() ? body.accountId.trim() : "default");
6932
+ accountId = extractAccountId(body.accountId);
6890
6933
  } catch (err) {
6891
- res.status(400).json({ error: err.message });
6892
- return;
6893
- }
6894
- const session = signalPairingSessions.get(accountId);
6895
- if (session) {
6896
- session.stop();
6897
- signalPairingSessions.delete(accountId);
6898
- }
6899
- signalPairingSnapshots.delete(accountId);
6900
- res.status(200).json({ ok: true, accountId, status: "idle" });
6901
- }
6902
- async function handleDisconnect(req, res, runtime) {
6903
- const body = req.body ?? {};
6904
- let accountId;
6905
- try {
6906
- accountId = sanitizeAccountId(typeof body.accountId === "string" && body.accountId.trim() ? body.accountId.trim() : "default");
6907
- } catch (err) {
6908
- res.status(400).json({ error: err.message });
6934
+ res.status(400).json(setupError("bad_request", err.message));
6909
6935
  return;
6910
6936
  }
6911
6937
  const session = signalPairingSessions.get(accountId);
@@ -6919,9 +6945,7 @@ async function handleDisconnect(req, res, runtime) {
6919
6945
  try {
6920
6946
  signalLogout(workspaceDir, accountId);
6921
6947
  } catch (err) {
6922
- res.status(500).json({
6923
- error: `Failed to disconnect Signal: ${String(err)}`
6924
- });
6948
+ res.status(500).json(setupError("internal_error", `Failed to disconnect Signal: ${err instanceof Error ? err.message : String(err)}`));
6925
6949
  return;
6926
6950
  }
6927
6951
  if (setupService) {
@@ -6943,37 +6967,33 @@ async function handleDisconnect(req, res, runtime) {
6943
6967
  };
6944
6968
  });
6945
6969
  } catch (error) {
6946
- res.status(500).json({
6947
- error: `Failed to persist Signal disconnect: ${String(error)}`
6948
- });
6970
+ res.status(500).json(setupError("internal_error", `Failed to persist Signal disconnect: ${error instanceof Error ? error.message : String(error)}`));
6949
6971
  return;
6950
6972
  }
6951
6973
  }
6952
- res.status(200).json({ ok: true, accountId });
6974
+ res.status(200).json({
6975
+ connector: "signal",
6976
+ state: "idle",
6977
+ detail: { accountId }
6978
+ });
6953
6979
  }
6954
6980
  var signalSetupRoutes = [
6955
- {
6956
- type: "POST",
6957
- path: "/api/signal/pair",
6958
- handler: handlePair,
6959
- rawPath: true
6960
- },
6961
6981
  {
6962
6982
  type: "GET",
6963
- path: "/api/signal/status",
6983
+ path: "/api/setup/signal/status",
6964
6984
  handler: handleStatus,
6965
6985
  rawPath: true
6966
6986
  },
6967
6987
  {
6968
6988
  type: "POST",
6969
- path: "/api/signal/pair/stop",
6970
- handler: handlePairStop,
6989
+ path: "/api/setup/signal/start",
6990
+ handler: handleStart,
6971
6991
  rawPath: true
6972
6992
  },
6973
6993
  {
6974
6994
  type: "POST",
6975
- path: "/api/signal/disconnect",
6976
- handler: handleDisconnect,
6995
+ path: "/api/setup/signal/cancel",
6996
+ handler: handleCancel,
6977
6997
  rawPath: true
6978
6998
  }
6979
6999
  ];
@@ -7023,6 +7043,74 @@ class SignalWorkflowCredentialProvider extends Service2 {
7023
7043
  };
7024
7044
  }
7025
7045
  }
7046
+ // src/local-client.ts
7047
+ import { randomUUID } from "node:crypto";
7048
+ var DEFAULT_SIGNAL_HTTP_URL = "http://127.0.0.1:8080";
7049
+ var DEFAULT_RECEIVE_LIMIT = 25;
7050
+ var MAX_RECEIVE_LIMIT = 100;
7051
+ function isRecord2(value) {
7052
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
7053
+ }
7054
+ function isSignalCliReceiveResponse(value) {
7055
+ return isRecord2(value);
7056
+ }
7057
+ function readSignalLocalClientConfigFromEnv(env = process.env) {
7058
+ const accountNumber = env.SIGNAL_ACCOUNT_NUMBER?.trim();
7059
+ if (!accountNumber)
7060
+ return null;
7061
+ return {
7062
+ httpUrl: env.SIGNAL_HTTP_URL?.trim() || DEFAULT_SIGNAL_HTTP_URL,
7063
+ accountNumber
7064
+ };
7065
+ }
7066
+ async function readSignalInboundMessages(config, limit = DEFAULT_RECEIVE_LIMIT) {
7067
+ const parsedLimit = Number.isFinite(limit) ? Math.floor(limit) : DEFAULT_RECEIVE_LIMIT;
7068
+ const receiveLimit = Math.min(Math.max(1, parsedLimit), MAX_RECEIVE_LIMIT);
7069
+ const baseUrl = config.httpUrl.replace(/\/$/, "");
7070
+ const account = encodeURIComponent(config.accountNumber);
7071
+ const response = await fetch(`${baseUrl}/v1/receive/${account}`, {
7072
+ headers: { Accept: "application/json" }
7073
+ });
7074
+ if (!response.ok) {
7075
+ throw new Error(`Signal local receive failed with HTTP ${response.status}`);
7076
+ }
7077
+ const body = await response.json();
7078
+ if (!Array.isArray(body)) {
7079
+ throw new Error("Signal local receive returned an unexpected payload");
7080
+ }
7081
+ const messages = [];
7082
+ for (const item of body.filter(isSignalCliReceiveResponse)) {
7083
+ if (messages.length >= receiveLimit)
7084
+ break;
7085
+ const envelope = item.envelope;
7086
+ const dataMessage = envelope?.dataMessage;
7087
+ const text = dataMessage?.message?.trim();
7088
+ if (!envelope || !dataMessage || !text)
7089
+ continue;
7090
+ const senderNumber = envelope.sourceNumber ?? envelope.source ?? null;
7091
+ const senderUuid = envelope.sourceUuid ?? null;
7092
+ const groupId = dataMessage.groupInfo?.groupId ?? null;
7093
+ const isGroup = Boolean(groupId);
7094
+ const channelId = groupId ?? senderNumber ?? senderUuid;
7095
+ if (!channelId)
7096
+ continue;
7097
+ const speakerName = envelope.sourceName ?? senderNumber ?? senderUuid ?? "Signal";
7098
+ const createdAt = dataMessage.timestamp ?? envelope.timestamp ?? Date.now();
7099
+ const threadId = `signal:${channelId}`;
7100
+ messages.push({
7101
+ id: `${threadId}:${createdAt}:${randomUUID()}`,
7102
+ roomId: threadId,
7103
+ channelId,
7104
+ roomName: isGroup ? `Signal group ${channelId}` : speakerName,
7105
+ speakerName,
7106
+ text,
7107
+ createdAt,
7108
+ isFromAgent: senderNumber === config.accountNumber,
7109
+ isGroup
7110
+ });
7111
+ }
7112
+ return messages;
7113
+ }
7026
7114
 
7027
7115
  // src/index.ts
7028
7116
  var signalPlugin = {
@@ -7048,7 +7136,7 @@ var signalPlugin = {
7048
7136
  const accountNumber = runtime.getSetting("SIGNAL_ACCOUNT_NUMBER");
7049
7137
  const httpUrl = runtime.getSetting("SIGNAL_HTTP_URL");
7050
7138
  const cliPath = runtime.getSetting("SIGNAL_CLI_PATH");
7051
- const effectiveCliPath = (cliPath ?? "").trim() || DEFAULT_SIGNAL_CLI_PATH;
7139
+ const effectiveCliPath = cliPath.trim() || DEFAULT_SIGNAL_CLI_PATH;
7052
7140
  const ignoreGroups = runtime.getSetting("SIGNAL_SHOULD_IGNORE_GROUP_MESSAGES");
7053
7141
  const maskNumber = (number) => {
7054
7142
  if (!number || number.trim() === "")
@@ -7082,6 +7170,10 @@ var signalPlugin = {
7082
7170
  mode: httpUrl ? "http" : "local-cli",
7083
7171
  cliPath: effectiveCliPath
7084
7172
  }, "Signal plugin configuration validated successfully");
7173
+ },
7174
+ async dispose(runtime) {
7175
+ const svc = runtime.getService(SignalService.serviceType);
7176
+ await svc?.stop();
7085
7177
  }
7086
7178
  };
7087
7179
  var src_default = signalPlugin;
@@ -7103,6 +7195,8 @@ export {
7103
7195
  sanitizeAccountId as sanitizeSignalAccountId,
7104
7196
  resolveSignalAccount,
7105
7197
  resolveDefaultSignalAccountId,
7198
+ readSignalLocalClientConfigFromEnv,
7199
+ readSignalInboundMessages,
7106
7200
  parseSignalEventData,
7107
7201
  parseSignalCliAccountsOutput,
7108
7202
  normalizeE164,
@@ -7136,4 +7230,4 @@ export {
7136
7230
  DEFAULT_ACCOUNT_ID
7137
7231
  };
7138
7232
 
7139
- //# debugId=593E95DA16C8A67B64756E2164756E21
7233
+ //# debugId=A2D8D2D244D7703264756E2164756E21