@teneo-protocol/sdk 1.0.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 (281) hide show
  1. package/.dockerignore +14 -0
  2. package/.env.test.example +14 -0
  3. package/.eslintrc.json +26 -0
  4. package/.github/workflows/claude-code-review.yml +78 -0
  5. package/.github/workflows/claude-reviewer.yml +64 -0
  6. package/.github/workflows/publish-npm.yml +38 -0
  7. package/.github/workflows/push-to-main.yml +23 -0
  8. package/.node-version +1 -0
  9. package/.prettierrc +11 -0
  10. package/Dockerfile +25 -0
  11. package/LICENCE +661 -0
  12. package/README.md +709 -0
  13. package/dist/constants.d.ts +42 -0
  14. package/dist/constants.d.ts.map +1 -0
  15. package/dist/constants.js +45 -0
  16. package/dist/constants.js.map +1 -0
  17. package/dist/core/websocket-client.d.ts +261 -0
  18. package/dist/core/websocket-client.d.ts.map +1 -0
  19. package/dist/core/websocket-client.js +875 -0
  20. package/dist/core/websocket-client.js.map +1 -0
  21. package/dist/formatters/response-formatter.d.ts +354 -0
  22. package/dist/formatters/response-formatter.d.ts.map +1 -0
  23. package/dist/formatters/response-formatter.js +575 -0
  24. package/dist/formatters/response-formatter.js.map +1 -0
  25. package/dist/handlers/message-handler-registry.d.ts +155 -0
  26. package/dist/handlers/message-handler-registry.d.ts.map +1 -0
  27. package/dist/handlers/message-handler-registry.js +216 -0
  28. package/dist/handlers/message-handler-registry.js.map +1 -0
  29. package/dist/handlers/message-handlers/agent-selected-handler.d.ts +112 -0
  30. package/dist/handlers/message-handlers/agent-selected-handler.d.ts.map +1 -0
  31. package/dist/handlers/message-handlers/agent-selected-handler.js +40 -0
  32. package/dist/handlers/message-handlers/agent-selected-handler.js.map +1 -0
  33. package/dist/handlers/message-handlers/agents-list-handler.d.ts +14 -0
  34. package/dist/handlers/message-handlers/agents-list-handler.d.ts.map +1 -0
  35. package/dist/handlers/message-handlers/agents-list-handler.js +25 -0
  36. package/dist/handlers/message-handlers/agents-list-handler.js.map +1 -0
  37. package/dist/handlers/message-handlers/auth-error-handler.d.ts +71 -0
  38. package/dist/handlers/message-handlers/auth-error-handler.d.ts.map +1 -0
  39. package/dist/handlers/message-handlers/auth-error-handler.js +30 -0
  40. package/dist/handlers/message-handlers/auth-error-handler.js.map +1 -0
  41. package/dist/handlers/message-handlers/auth-message-handler.d.ts +18 -0
  42. package/dist/handlers/message-handlers/auth-message-handler.d.ts.map +1 -0
  43. package/dist/handlers/message-handlers/auth-message-handler.js +60 -0
  44. package/dist/handlers/message-handlers/auth-message-handler.js.map +1 -0
  45. package/dist/handlers/message-handlers/auth-required-handler.d.ts +76 -0
  46. package/dist/handlers/message-handlers/auth-required-handler.d.ts.map +1 -0
  47. package/dist/handlers/message-handlers/auth-required-handler.js +23 -0
  48. package/dist/handlers/message-handlers/auth-required-handler.js.map +1 -0
  49. package/dist/handlers/message-handlers/auth-success-handler.d.ts +18 -0
  50. package/dist/handlers/message-handlers/auth-success-handler.d.ts.map +1 -0
  51. package/dist/handlers/message-handlers/auth-success-handler.js +51 -0
  52. package/dist/handlers/message-handlers/auth-success-handler.js.map +1 -0
  53. package/dist/handlers/message-handlers/base-handler.d.ts +55 -0
  54. package/dist/handlers/message-handlers/base-handler.d.ts.map +1 -0
  55. package/dist/handlers/message-handlers/base-handler.js +83 -0
  56. package/dist/handlers/message-handlers/base-handler.js.map +1 -0
  57. package/dist/handlers/message-handlers/challenge-handler.d.ts +73 -0
  58. package/dist/handlers/message-handlers/challenge-handler.d.ts.map +1 -0
  59. package/dist/handlers/message-handlers/challenge-handler.js +47 -0
  60. package/dist/handlers/message-handlers/challenge-handler.js.map +1 -0
  61. package/dist/handlers/message-handlers/error-message-handler.d.ts +76 -0
  62. package/dist/handlers/message-handlers/error-message-handler.d.ts.map +1 -0
  63. package/dist/handlers/message-handlers/error-message-handler.js +29 -0
  64. package/dist/handlers/message-handlers/error-message-handler.js.map +1 -0
  65. package/dist/handlers/message-handlers/index.d.ts +28 -0
  66. package/dist/handlers/message-handlers/index.d.ts.map +1 -0
  67. package/dist/handlers/message-handlers/index.js +100 -0
  68. package/dist/handlers/message-handlers/index.js.map +1 -0
  69. package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts +122 -0
  70. package/dist/handlers/message-handlers/list-rooms-response-handler.d.ts.map +1 -0
  71. package/dist/handlers/message-handlers/list-rooms-response-handler.js +30 -0
  72. package/dist/handlers/message-handlers/list-rooms-response-handler.js.map +1 -0
  73. package/dist/handlers/message-handlers/ping-pong-handler.d.ts +104 -0
  74. package/dist/handlers/message-handlers/ping-pong-handler.d.ts.map +1 -0
  75. package/dist/handlers/message-handlers/ping-pong-handler.js +36 -0
  76. package/dist/handlers/message-handlers/ping-pong-handler.js.map +1 -0
  77. package/dist/handlers/message-handlers/regular-message-handler.d.ts +56 -0
  78. package/dist/handlers/message-handlers/regular-message-handler.d.ts.map +1 -0
  79. package/dist/handlers/message-handlers/regular-message-handler.js +59 -0
  80. package/dist/handlers/message-handlers/regular-message-handler.js.map +1 -0
  81. package/dist/handlers/message-handlers/subscribe-response-handler.d.ts +81 -0
  82. package/dist/handlers/message-handlers/subscribe-response-handler.d.ts.map +1 -0
  83. package/dist/handlers/message-handlers/subscribe-response-handler.js +48 -0
  84. package/dist/handlers/message-handlers/subscribe-response-handler.js.map +1 -0
  85. package/dist/handlers/message-handlers/task-response-handler.d.ts +14 -0
  86. package/dist/handlers/message-handlers/task-response-handler.d.ts.map +1 -0
  87. package/dist/handlers/message-handlers/task-response-handler.js +44 -0
  88. package/dist/handlers/message-handlers/task-response-handler.js.map +1 -0
  89. package/dist/handlers/message-handlers/types.d.ts +51 -0
  90. package/dist/handlers/message-handlers/types.d.ts.map +1 -0
  91. package/dist/handlers/message-handlers/types.js +7 -0
  92. package/dist/handlers/message-handlers/types.js.map +1 -0
  93. package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts +81 -0
  94. package/dist/handlers/message-handlers/unsubscribe-response-handler.d.ts.map +1 -0
  95. package/dist/handlers/message-handlers/unsubscribe-response-handler.js +48 -0
  96. package/dist/handlers/message-handlers/unsubscribe-response-handler.js.map +1 -0
  97. package/dist/handlers/webhook-handler.d.ts +202 -0
  98. package/dist/handlers/webhook-handler.d.ts.map +1 -0
  99. package/dist/handlers/webhook-handler.js +511 -0
  100. package/dist/handlers/webhook-handler.js.map +1 -0
  101. package/dist/index.d.ts +71 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +217 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/managers/agent-registry.d.ts +173 -0
  106. package/dist/managers/agent-registry.d.ts.map +1 -0
  107. package/dist/managers/agent-registry.js +310 -0
  108. package/dist/managers/agent-registry.js.map +1 -0
  109. package/dist/managers/connection-manager.d.ts +134 -0
  110. package/dist/managers/connection-manager.d.ts.map +1 -0
  111. package/dist/managers/connection-manager.js +176 -0
  112. package/dist/managers/connection-manager.js.map +1 -0
  113. package/dist/managers/index.d.ts +9 -0
  114. package/dist/managers/index.d.ts.map +1 -0
  115. package/dist/managers/index.js +16 -0
  116. package/dist/managers/index.js.map +1 -0
  117. package/dist/managers/message-router.d.ts +112 -0
  118. package/dist/managers/message-router.d.ts.map +1 -0
  119. package/dist/managers/message-router.js +260 -0
  120. package/dist/managers/message-router.js.map +1 -0
  121. package/dist/managers/room-manager.d.ts +165 -0
  122. package/dist/managers/room-manager.d.ts.map +1 -0
  123. package/dist/managers/room-manager.js +227 -0
  124. package/dist/managers/room-manager.js.map +1 -0
  125. package/dist/teneo-sdk.d.ts +703 -0
  126. package/dist/teneo-sdk.d.ts.map +1 -0
  127. package/dist/teneo-sdk.js +907 -0
  128. package/dist/teneo-sdk.js.map +1 -0
  129. package/dist/types/config.d.ts +1047 -0
  130. package/dist/types/config.d.ts.map +1 -0
  131. package/dist/types/config.js +720 -0
  132. package/dist/types/config.js.map +1 -0
  133. package/dist/types/error-codes.d.ts +29 -0
  134. package/dist/types/error-codes.d.ts.map +1 -0
  135. package/dist/types/error-codes.js +41 -0
  136. package/dist/types/error-codes.js.map +1 -0
  137. package/dist/types/events.d.ts +616 -0
  138. package/dist/types/events.d.ts.map +1 -0
  139. package/dist/types/events.js +261 -0
  140. package/dist/types/events.js.map +1 -0
  141. package/dist/types/health.d.ts +40 -0
  142. package/dist/types/health.d.ts.map +1 -0
  143. package/dist/types/health.js +6 -0
  144. package/dist/types/health.js.map +1 -0
  145. package/dist/types/index.d.ts +10 -0
  146. package/dist/types/index.d.ts.map +1 -0
  147. package/dist/types/index.js +123 -0
  148. package/dist/types/index.js.map +1 -0
  149. package/dist/types/messages.d.ts +3734 -0
  150. package/dist/types/messages.d.ts.map +1 -0
  151. package/dist/types/messages.js +482 -0
  152. package/dist/types/messages.js.map +1 -0
  153. package/dist/types/validation.d.ts +81 -0
  154. package/dist/types/validation.d.ts.map +1 -0
  155. package/dist/types/validation.js +115 -0
  156. package/dist/types/validation.js.map +1 -0
  157. package/dist/utils/bounded-queue.d.ts +127 -0
  158. package/dist/utils/bounded-queue.d.ts.map +1 -0
  159. package/dist/utils/bounded-queue.js +181 -0
  160. package/dist/utils/bounded-queue.js.map +1 -0
  161. package/dist/utils/circuit-breaker.d.ts +141 -0
  162. package/dist/utils/circuit-breaker.d.ts.map +1 -0
  163. package/dist/utils/circuit-breaker.js +215 -0
  164. package/dist/utils/circuit-breaker.js.map +1 -0
  165. package/dist/utils/deduplication-cache.d.ts +110 -0
  166. package/dist/utils/deduplication-cache.d.ts.map +1 -0
  167. package/dist/utils/deduplication-cache.js +177 -0
  168. package/dist/utils/deduplication-cache.js.map +1 -0
  169. package/dist/utils/event-waiter.d.ts +101 -0
  170. package/dist/utils/event-waiter.d.ts.map +1 -0
  171. package/dist/utils/event-waiter.js +118 -0
  172. package/dist/utils/event-waiter.js.map +1 -0
  173. package/dist/utils/index.d.ts +51 -0
  174. package/dist/utils/index.d.ts.map +1 -0
  175. package/dist/utils/index.js +72 -0
  176. package/dist/utils/index.js.map +1 -0
  177. package/dist/utils/logger.d.ts +22 -0
  178. package/dist/utils/logger.d.ts.map +1 -0
  179. package/dist/utils/logger.js +91 -0
  180. package/dist/utils/logger.js.map +1 -0
  181. package/dist/utils/rate-limiter.d.ts +122 -0
  182. package/dist/utils/rate-limiter.d.ts.map +1 -0
  183. package/dist/utils/rate-limiter.js +190 -0
  184. package/dist/utils/rate-limiter.js.map +1 -0
  185. package/dist/utils/retry-policy.d.ts +191 -0
  186. package/dist/utils/retry-policy.d.ts.map +1 -0
  187. package/dist/utils/retry-policy.js +225 -0
  188. package/dist/utils/retry-policy.js.map +1 -0
  189. package/dist/utils/secure-private-key.d.ts +113 -0
  190. package/dist/utils/secure-private-key.d.ts.map +1 -0
  191. package/dist/utils/secure-private-key.js +188 -0
  192. package/dist/utils/secure-private-key.js.map +1 -0
  193. package/dist/utils/signature-verifier.d.ts +143 -0
  194. package/dist/utils/signature-verifier.d.ts.map +1 -0
  195. package/dist/utils/signature-verifier.js +238 -0
  196. package/dist/utils/signature-verifier.js.map +1 -0
  197. package/dist/utils/ssrf-validator.d.ts +36 -0
  198. package/dist/utils/ssrf-validator.d.ts.map +1 -0
  199. package/dist/utils/ssrf-validator.js +195 -0
  200. package/dist/utils/ssrf-validator.js.map +1 -0
  201. package/examples/.env.example +17 -0
  202. package/examples/basic-usage.ts +211 -0
  203. package/examples/production-dashboard/.env.example +153 -0
  204. package/examples/production-dashboard/package.json +39 -0
  205. package/examples/production-dashboard/public/dashboard.html +642 -0
  206. package/examples/production-dashboard/server.ts +753 -0
  207. package/examples/webhook-integration.ts +239 -0
  208. package/examples/x-influencer-battle-redesign.html +1065 -0
  209. package/examples/x-influencer-battle-server.ts +217 -0
  210. package/examples/x-influencer-battle.html +787 -0
  211. package/package.json +65 -0
  212. package/src/constants.ts +43 -0
  213. package/src/core/websocket-client.test.ts +512 -0
  214. package/src/core/websocket-client.ts +1056 -0
  215. package/src/formatters/response-formatter.test.ts +571 -0
  216. package/src/formatters/response-formatter.ts +677 -0
  217. package/src/handlers/message-handler-registry.ts +239 -0
  218. package/src/handlers/message-handlers/agent-selected-handler.ts +40 -0
  219. package/src/handlers/message-handlers/agents-list-handler.ts +26 -0
  220. package/src/handlers/message-handlers/auth-error-handler.ts +31 -0
  221. package/src/handlers/message-handlers/auth-message-handler.ts +66 -0
  222. package/src/handlers/message-handlers/auth-required-handler.ts +23 -0
  223. package/src/handlers/message-handlers/auth-success-handler.ts +57 -0
  224. package/src/handlers/message-handlers/base-handler.ts +101 -0
  225. package/src/handlers/message-handlers/challenge-handler.ts +57 -0
  226. package/src/handlers/message-handlers/error-message-handler.ts +27 -0
  227. package/src/handlers/message-handlers/index.ts +77 -0
  228. package/src/handlers/message-handlers/list-rooms-response-handler.ts +28 -0
  229. package/src/handlers/message-handlers/ping-pong-handler.ts +30 -0
  230. package/src/handlers/message-handlers/regular-message-handler.ts +65 -0
  231. package/src/handlers/message-handlers/subscribe-response-handler.ts +47 -0
  232. package/src/handlers/message-handlers/task-response-handler.ts +45 -0
  233. package/src/handlers/message-handlers/types.ts +77 -0
  234. package/src/handlers/message-handlers/unsubscribe-response-handler.ts +47 -0
  235. package/src/handlers/webhook-handler.test.ts +789 -0
  236. package/src/handlers/webhook-handler.ts +576 -0
  237. package/src/index.ts +269 -0
  238. package/src/managers/agent-registry.test.ts +466 -0
  239. package/src/managers/agent-registry.ts +347 -0
  240. package/src/managers/connection-manager.ts +195 -0
  241. package/src/managers/index.ts +9 -0
  242. package/src/managers/message-router.ts +349 -0
  243. package/src/managers/room-manager.ts +248 -0
  244. package/src/teneo-sdk.ts +1022 -0
  245. package/src/types/config.test.ts +325 -0
  246. package/src/types/config.ts +799 -0
  247. package/src/types/error-codes.ts +44 -0
  248. package/src/types/events.test.ts +302 -0
  249. package/src/types/events.ts +382 -0
  250. package/src/types/health.ts +46 -0
  251. package/src/types/index.ts +199 -0
  252. package/src/types/messages.test.ts +660 -0
  253. package/src/types/messages.ts +570 -0
  254. package/src/types/validation.ts +123 -0
  255. package/src/utils/bounded-queue.test.ts +356 -0
  256. package/src/utils/bounded-queue.ts +205 -0
  257. package/src/utils/circuit-breaker.test.ts +394 -0
  258. package/src/utils/circuit-breaker.ts +262 -0
  259. package/src/utils/deduplication-cache.test.ts +380 -0
  260. package/src/utils/deduplication-cache.ts +198 -0
  261. package/src/utils/event-waiter.test.ts +381 -0
  262. package/src/utils/event-waiter.ts +172 -0
  263. package/src/utils/index.ts +74 -0
  264. package/src/utils/logger.ts +87 -0
  265. package/src/utils/rate-limiter.test.ts +341 -0
  266. package/src/utils/rate-limiter.ts +211 -0
  267. package/src/utils/retry-policy.test.ts +558 -0
  268. package/src/utils/retry-policy.ts +272 -0
  269. package/src/utils/secure-private-key.test.ts +356 -0
  270. package/src/utils/secure-private-key.ts +205 -0
  271. package/src/utils/signature-verifier.test.ts +464 -0
  272. package/src/utils/signature-verifier.ts +298 -0
  273. package/src/utils/ssrf-validator.test.ts +372 -0
  274. package/src/utils/ssrf-validator.ts +224 -0
  275. package/tests/integration/real-server.test.ts +740 -0
  276. package/tests/integration/websocket.test.ts +381 -0
  277. package/tests/integration-setup.ts +16 -0
  278. package/tests/setup.ts +34 -0
  279. package/tsconfig.json +32 -0
  280. package/vitest.config.ts +42 -0
  281. package/vitest.integration.config.ts +23 -0
@@ -0,0 +1,349 @@
1
+ /**
2
+ * MessageRouter - Manages message sending and routing
3
+ * Handles user messages, direct commands, and message-response patterns
4
+ */
5
+
6
+ import { EventEmitter } from "eventemitter3";
7
+ import { v4 as uuidv4 } from "uuid";
8
+ import { WebSocketClient } from "../core/websocket-client";
9
+ import { WebhookHandler } from "../handlers/webhook-handler";
10
+ import { ResponseFormatter, FormattedResponse } from "../formatters/response-formatter";
11
+ import {
12
+ UserMessage,
13
+ createUserMessage,
14
+ Logger,
15
+ ResponseFormat,
16
+ TaskResponseMessage
17
+ } from "../types";
18
+ import { SDKEvents, SDKError, ValidationError, AgentResponse } from "../types/events";
19
+ import { ErrorCode } from "../types/error-codes";
20
+ import { TIMEOUTS } from "../constants";
21
+ import {
22
+ MessageContentSchema,
23
+ AgentIdSchema,
24
+ AgentCommandContentSchema
25
+ } from "../types/validation";
26
+ import { waitForEvent } from "../utils/event-waiter";
27
+
28
+ export interface SendMessageOptions {
29
+ room: string;
30
+ from?: string;
31
+ waitForResponse?: boolean;
32
+ timeout?: number;
33
+ format?: ResponseFormat | "raw" | "humanized";
34
+ }
35
+
36
+ export interface AgentCommand {
37
+ agent: string;
38
+ command: string;
39
+ room: string;
40
+ }
41
+
42
+ export class MessageRouter extends EventEmitter<SDKEvents> {
43
+ private readonly wsClient: WebSocketClient;
44
+ private readonly webhookHandler: WebhookHandler;
45
+ private readonly responseFormatter: ResponseFormatter;
46
+ private readonly logger: Logger;
47
+ private readonly messageTimeout: number;
48
+ private readonly responseFormat: ResponseFormat;
49
+
50
+ constructor(
51
+ wsClient: WebSocketClient,
52
+ webhookHandler: WebhookHandler,
53
+ responseFormatter: ResponseFormatter,
54
+ logger: Logger,
55
+ config: {
56
+ messageTimeout?: number;
57
+ responseFormat?: ResponseFormat;
58
+ }
59
+ ) {
60
+ super();
61
+ this.wsClient = wsClient;
62
+ this.webhookHandler = webhookHandler;
63
+ this.responseFormatter = responseFormatter;
64
+ this.logger = logger;
65
+ this.messageTimeout = config.messageTimeout ?? TIMEOUTS.DEFAULT_MESSAGE_TIMEOUT;
66
+ this.responseFormat = config.responseFormat ?? "humanized";
67
+
68
+ this.setupEventForwarding();
69
+ }
70
+
71
+ /**
72
+ * Sends a message to agents via the coordinator.
73
+ * The coordinator intelligently selects the most appropriate agent.
74
+ * Supports optional response waiting with configurable timeout and format.
75
+ *
76
+ * @param content - The message content to send
77
+ * @param options - Configuration for message sending
78
+ * @param options.room - Room to send to (required)
79
+ * @param options.from - Sender address (defaults to authenticated wallet)
80
+ * @param options.waitForResponse - Whether to wait for response (default: false)
81
+ * @param options.timeout - Response timeout in ms (default: 60000)
82
+ * @param options.format - Response format: 'raw', 'humanized', or 'both'
83
+ * @returns Promise resolving to FormattedResponse if waiting, void otherwise
84
+ * @throws {SDKError} If not connected
85
+ * @throws {ValidationError} If content is empty, room not specified, or options invalid
86
+ * @throws {TimeoutError} If waitForResponse and timeout exceeded
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * // Fire and forget
91
+ * await messageRouter.sendMessage('What is the weather?', { room: 'general' });
92
+ *
93
+ * // Wait for response
94
+ * const response = await messageRouter.sendMessage('Calculate 2+2', {
95
+ * room: 'general',
96
+ * waitForResponse: true,
97
+ * timeout: 30000
98
+ * });
99
+ * console.log(response.humanized);
100
+ * ```
101
+ */
102
+ public async sendMessage(
103
+ content: string,
104
+ options: SendMessageOptions
105
+ ): Promise<FormattedResponse | void> {
106
+ if (!this.wsClient.isConnected) {
107
+ throw new SDKError("Not connected to Teneo network", ErrorCode.NOT_CONNECTED);
108
+ }
109
+
110
+ // Validate content
111
+ const validatedContent = MessageContentSchema.parse(content);
112
+
113
+ const room = options.room;
114
+ if (!room) {
115
+ throw new ValidationError("Room parameter is required");
116
+ }
117
+
118
+ // Use custom 'from' address if provided, otherwise use wallet address from auth state
119
+ const authState = this.wsClient.getAuthState();
120
+ const fromAddress = options.from ?? authState.walletAddress;
121
+
122
+ const message = createUserMessage(validatedContent, room, fromAddress);
123
+
124
+ this.logger.debug("MessageRouter: Sending message", {
125
+ content: validatedContent,
126
+ room,
127
+ from: fromAddress
128
+ });
129
+
130
+ if (options.waitForResponse) {
131
+ return await this.sendMessageAndWaitForResponse(message, options);
132
+ } else {
133
+ await this.wsClient.sendMessage(message);
134
+ await this.webhookHandler.sendMessageWebhook(message);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Sends a direct command to a specific agent, bypassing the coordinator.
140
+ * Formats command as "@agentName command" internally.
141
+ *
142
+ * @param command - The direct agent command configuration
143
+ * @param command.agent - Agent ID or name to send command to
144
+ * @param command.command - Command text to send
145
+ * @param command.room - Room to send in (required)
146
+ * @returns Promise that resolves when command is sent
147
+ * @throws {SDKError} If not connected
148
+ * @throws {ValidationError} If agent/command empty or room not specified
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * await messageRouter.sendDirectCommand({
153
+ * agent: 'weather-agent',
154
+ * command: 'Get forecast for Tokyo',
155
+ * room: 'general'
156
+ * });
157
+ * ```
158
+ */
159
+ public async sendDirectCommand(command: AgentCommand): Promise<FormattedResponse | void> {
160
+ if (!this.wsClient.isConnected) {
161
+ throw new SDKError("Not connected to Teneo network", ErrorCode.NOT_CONNECTED);
162
+ }
163
+
164
+ // Validate command
165
+ const validatedAgent = AgentIdSchema.parse(command.agent);
166
+ const validatedCommand = AgentCommandContentSchema.parse(command.command);
167
+
168
+ const room = command.room;
169
+ if (!room) {
170
+ throw new ValidationError("Room parameter is required");
171
+ }
172
+
173
+ // Get wallet address from auth state
174
+ const authState = this.wsClient.getAuthState();
175
+ const walletAddress = authState.walletAddress;
176
+
177
+ // Format as direct command
178
+ const content = `@${validatedAgent} ${validatedCommand}`;
179
+ const message = createUserMessage(content, room, walletAddress);
180
+
181
+ this.logger.debug("MessageRouter: Sending direct command", {
182
+ agent: validatedAgent,
183
+ command: validatedCommand,
184
+ room,
185
+ from: walletAddress
186
+ });
187
+
188
+ await this.wsClient.sendMessage(message);
189
+ await this.webhookHandler.sendMessageWebhook(message);
190
+ }
191
+
192
+
193
+
194
+ /**
195
+ * Send message and wait for agent response
196
+ * Uses event-waiter utility for clean Promise-based waiting with automatic cleanup
197
+ */
198
+ private async sendMessageAndWaitForResponse(
199
+ message: UserMessage,
200
+ options: SendMessageOptions
201
+ ): Promise<FormattedResponse> {
202
+ // Generate unique request ID for correlation
203
+ const requestId = uuidv4();
204
+ const timeout = options.timeout ?? this.messageTimeout;
205
+ const format = options.format ?? this.responseFormat;
206
+
207
+ // Add client_request_id to message data for server-side correlation
208
+ const messageWithId: UserMessage = {
209
+ ...message,
210
+ data: {
211
+ ...(message.data || {}),
212
+ client_request_id: requestId
213
+ }
214
+ };
215
+
216
+ this.logger.debug("Sending message with request tracking", { requestId });
217
+
218
+ // Send message first (fail fast if send fails)
219
+ await this.wsClient.sendMessage(messageWithId);
220
+
221
+ // Wait for agent response with automatic timeout and cleanup
222
+ // The filter ensures we only match responses for THIS specific request
223
+ const response = await waitForEvent<AgentResponse>(this.wsClient, "agent:response", {
224
+ timeout,
225
+ filter: (r) => {
226
+ // Try to match by client_request_id if server echoes it back
227
+ const responseRequestId =
228
+ r.raw?.data && "client_request_id" in r.raw.data
229
+ ? (r.raw.data as any).client_request_id
230
+ : undefined;
231
+
232
+ return responseRequestId === requestId;
233
+ },
234
+ timeoutMessage: `Message timeout - no response received after ${timeout}ms (requestId: ${requestId})`
235
+ });
236
+
237
+ // Format response according to requested format
238
+ if (format === "raw" && response.raw) {
239
+ return response.raw as unknown as FormattedResponse;
240
+ }
241
+ return response as FormattedResponse;
242
+ }
243
+
244
+ /**
245
+ * Set up event forwarding from WebSocket client
246
+ */
247
+ private setupEventForwarding(): void {
248
+ // Forward message events
249
+ this.wsClient.on("message:sent", (message) => {
250
+ this.emit("message:sent", message);
251
+ // Fire and forget - don't block event emission
252
+ this.webhookHandler.sendMessageWebhook(message).catch((error) => {
253
+ this.logger.error("Failed to send webhook for message:sent", error);
254
+ });
255
+ });
256
+
257
+ this.wsClient.on("message:received", (message) => {
258
+ this.emit("message:received", message);
259
+
260
+ // Send webhook for received messages (fire-and-forget)
261
+ if (message.type !== "ping" && message.type !== "pong") {
262
+ this.webhookHandler.sendMessageWebhook(message).catch((error) => {
263
+ this.logger.error("Failed to send webhook for message:received", error);
264
+ });
265
+ }
266
+ });
267
+
268
+ this.wsClient.on("message:error", (error, message) =>
269
+ this.emit("message:error", error, message)
270
+ );
271
+
272
+ // Forward agent events
273
+ this.wsClient.on("agent:selected", (data) => {
274
+ this.emit("agent:selected", data);
275
+ // Fire and forget - don't block event emission
276
+ this.webhookHandler
277
+ .sendWebhook("agent_selected", data, {
278
+ agentId: data.agentId
279
+ })
280
+ .catch((error) => {
281
+ this.logger.error("Failed to send webhook for agent:selected", error);
282
+ });
283
+ });
284
+
285
+ this.wsClient.on("agent:response", (response) => {
286
+ // Format response only if raw message is available
287
+ let enhancedResponse = response;
288
+
289
+ if (response.raw) {
290
+ try {
291
+ const formatted = this.responseFormatter.formatTaskResponse(
292
+ response.raw as TaskResponseMessage
293
+ );
294
+ enhancedResponse = {
295
+ ...response,
296
+ ...formatted
297
+ };
298
+ } catch (error) {
299
+ this.logger.debug("Could not format response, using original", {
300
+ error
301
+ });
302
+ }
303
+ }
304
+
305
+ // Emit event (waitForEvent listeners will receive this)
306
+ this.emit("agent:response", enhancedResponse);
307
+
308
+ // Fire and forget - don't block event emission
309
+ this.webhookHandler
310
+ .sendWebhook("task_response", enhancedResponse, {
311
+ agentId: response.agentId,
312
+ taskId: response.taskId
313
+ })
314
+ .catch((error) => {
315
+ this.logger.error("Failed to send webhook for agent:response", error);
316
+ });
317
+ });
318
+
319
+ // Forward coordinator events
320
+ this.wsClient.on("coordinator:processing", (request) =>
321
+ this.emit("coordinator:processing", request)
322
+ );
323
+ this.wsClient.on("coordinator:selected", (agentId, reasoning) =>
324
+ this.emit("coordinator:selected", agentId, reasoning)
325
+ );
326
+ this.wsClient.on("coordinator:error", (error) => this.emit("coordinator:error", error));
327
+ }
328
+
329
+ /**
330
+ * Destroys the message router and cleans up resources.
331
+ * Removes all event listeners and marks the router as destroyed.
332
+ * After destruction, the router cannot be reused.
333
+ *
334
+ * Note: Any pending waitForEvent calls will automatically timeout and clean up.
335
+ *
336
+ * @example
337
+ * ```typescript
338
+ * messageRouter.destroy();
339
+ * console.log('Message router destroyed');
340
+ * ```
341
+ */
342
+ public destroy(): void {
343
+ this.logger.info("MessageRouter: Destroying");
344
+
345
+ // Remove all event listeners
346
+ // Any pending waitForEvent calls will automatically timeout and clean up
347
+ this.removeAllListeners();
348
+ }
349
+ }
@@ -0,0 +1,248 @@
1
+ /**
2
+ * RoomManager - Manages room state and operations
3
+ * Handles joining, leaving, and tracking rooms
4
+ */
5
+
6
+ import { EventEmitter } from "eventemitter3";
7
+ import { WebSocketClient } from "../core/websocket-client";
8
+ import { Room, createSubscribe, createUnsubscribe, createListRooms, Logger, RoomInfo } from "../types";
9
+ import { SDKEvents, SDKError } from "../types/events";
10
+ import { ErrorCode } from "../types/error-codes";
11
+ import { RoomIdSchema } from "../types/validation";
12
+
13
+ export class RoomManager extends EventEmitter<SDKEvents> {
14
+ private readonly wsClient: WebSocketClient;
15
+ private readonly logger: Logger;
16
+ private readonly rooms = new Map<string, Room>();
17
+ private readonly subscribedRooms = new Set<string>();
18
+
19
+ constructor(wsClient: WebSocketClient, logger: Logger) {
20
+ super();
21
+ this.wsClient = wsClient;
22
+ this.logger = logger;
23
+ }
24
+
25
+ /**
26
+ * Subscribes to a public room in the Teneo network.
27
+ * Validates room ID and sends subscribe message to the server.
28
+ * The actual subscription state is updated when the server confirms via room:subscribed event.
29
+ *
30
+ * @param roomId - The ID of the room to subscribe to
31
+ * @returns Promise that resolves when subscribed
32
+ * @throws {SDKError} If not connected to the network
33
+ * @throws {ValidationError} If roomId is empty or invalid
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * await roomManager.subscribeToRoom('general');
38
+ * console.log('Subscription request sent to general room');
39
+ * ```
40
+ */
41
+ public async subscribeToRoom(roomId: string): Promise<void> {
42
+ if (!this.wsClient.isConnected) {
43
+ throw new SDKError("Not connected to Teneo network", ErrorCode.NOT_CONNECTED);
44
+ }
45
+
46
+ // Validate room ID
47
+ const validatedRoomId = RoomIdSchema.parse(roomId);
48
+
49
+ this.logger.info("RoomManager: Subscribing to room", { roomId: validatedRoomId });
50
+
51
+ const message = createSubscribe(validatedRoomId);
52
+ await this.wsClient.sendMessage(message);
53
+ }
54
+
55
+ /**
56
+ * Unsubscribes from a room in the Teneo network.
57
+ * Validates room ID and sends unsubscribe message to the server.
58
+ * The actual subscription state is updated when the server confirms via room:unsubscribed event.
59
+ *
60
+ * @param roomId - The ID of the room to unsubscribe from
61
+ * @returns Promise that resolves when unsubscribed
62
+ * @throws {SDKError} If not connected to the network
63
+ * @throws {ValidationError} If roomId is empty or invalid
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * await roomManager.unsubscribeFromRoom('general');
68
+ * console.log('Unsubscription request sent for general room');
69
+ * ```
70
+ */
71
+ public async unsubscribeFromRoom(roomId: string): Promise<void> {
72
+ if (!this.wsClient.isConnected) {
73
+ throw new SDKError("Not connected to Teneo network", ErrorCode.NOT_CONNECTED);
74
+ }
75
+
76
+ // Validate room ID
77
+ const validatedRoomId = RoomIdSchema.parse(roomId);
78
+
79
+ this.logger.info("RoomManager: Unsubscribing from room", { roomId: validatedRoomId });
80
+
81
+ const message = createUnsubscribe(validatedRoomId);
82
+ await this.wsClient.sendMessage(message);
83
+ }
84
+
85
+ /**
86
+ * Updates the subscription state from server response.
87
+ * This is the authoritative source of which rooms you're subscribed to.
88
+ * Called internally when receiving subscribe/unsubscribe responses.
89
+ *
90
+ * @internal
91
+ * @param subscriptions - Array of room IDs from server response
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * // Internal SDK usage
96
+ * roomManager.updateSubscriptions(['general', 'support', 'trading']);
97
+ * ```
98
+ */
99
+ public updateSubscriptions(subscriptions: string[]): void {
100
+ this.subscribedRooms.clear();
101
+ subscriptions.forEach(roomId => this.subscribedRooms.add(roomId));
102
+ this.logger.debug("RoomManager: Subscriptions updated", {
103
+ count: subscriptions.length,
104
+ rooms: subscriptions
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Gets all rooms currently subscribed to.
110
+ * Returns array of room IDs that you're actively listening to.
111
+ *
112
+ * @returns Array of subscribed room IDs
113
+ *
114
+ * @example
115
+ * ```typescript
116
+ * const rooms = roomManager.getSubscribedRooms();
117
+ * console.log(`Subscribed to ${rooms.length} rooms:`, rooms);
118
+ * ```
119
+ */
120
+ public getSubscribedRooms(): string[] {
121
+ return Array.from(this.subscribedRooms);
122
+ }
123
+
124
+ /**
125
+ * Gets a list of all available rooms.
126
+ * Returns rooms the user has access to based on authentication.
127
+ * Returns a read-only array with defensive copies to prevent external modification.
128
+ *
129
+ * @returns Read-only array of room copies
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * const rooms = roomManager.getRooms();
134
+ * rooms.forEach(room => console.log(`${room.id}: ${room.name}`));
135
+ * ```
136
+ */
137
+ public getRooms(): ReadonlyArray<Readonly<Room>> {
138
+ return Array.from(this.rooms.values()).map((room) => ({ ...room }));
139
+ }
140
+
141
+ /**
142
+ * Gets a specific room by its ID.
143
+ * Returns a defensive copy to prevent external modification of room state.
144
+ * Returns undefined if room doesn't exist or user doesn't have access.
145
+ *
146
+ * @param roomId - The ID of the room to retrieve
147
+ * @returns Copy of the room object if found, undefined otherwise
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * const room = roomManager.getRoom('general');
152
+ * if (room) {
153
+ * console.log(`Room: ${room.name}`);
154
+ * console.log(`Members: ${room.members?.length ?? 0}`);
155
+ * }
156
+ * ```
157
+ */
158
+ public getRoom(roomId: string): Readonly<Room> | undefined {
159
+ const room = this.rooms.get(roomId);
160
+ return room ? { ...room } : undefined;
161
+ }
162
+
163
+ /**
164
+ * Fetches the list of rooms from the server.
165
+ * Sends list_rooms message and waits for response from server.
166
+ * Returns array of rooms the user owns or has access to.
167
+ *
168
+ * @returns Promise that resolves to array of room information
169
+ * @throws {SDKError} If not connected to the network
170
+ *
171
+ * @example
172
+ * ```typescript
173
+ * const rooms = await roomManager.listRooms();
174
+ * rooms.forEach(room => {
175
+ * console.log(`${room.name} (${room.is_public ? 'public' : 'private'})`);
176
+ * console.log(`Owner: ${room.is_owner}`);
177
+ * });
178
+ * ```
179
+ */
180
+ public async listRooms(): Promise<RoomInfo[]> {
181
+ if (!this.wsClient.isConnected) {
182
+ throw new SDKError("Not connected to Teneo network", ErrorCode.NOT_CONNECTED);
183
+ }
184
+
185
+ this.logger.info("RoomManager: Listing rooms");
186
+
187
+ const message = createListRooms();
188
+ await this.wsClient.sendMessage(message);
189
+
190
+ // Room list will be received via list_rooms response handler
191
+ // For now, return empty array as this is async
192
+ // TODO: Implement response waiting mechanism
193
+ return [];
194
+ }
195
+
196
+ /**
197
+ * Updates the room list from authentication state.
198
+ * Called internally after successful authentication to populate accessible rooms.
199
+ *
200
+ * @internal This method is for internal SDK use
201
+ * @param rooms - Array of rooms to update in the manager
202
+ *
203
+ * @example
204
+ * ```typescript
205
+ * // Internal SDK usage after auth
206
+ * roomManager.updateRoomsFromAuth(authState.roomObjects);
207
+ * ```
208
+ */
209
+ public updateRoomsFromAuth(rooms: Room[]): void {
210
+ this.logger.debug("RoomManager: Updating rooms from auth", { count: rooms.length });
211
+
212
+ for (const room of rooms) {
213
+ this.rooms.set(room.id, room);
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Clears all rooms from the manager.
219
+ * Clears subscription state.
220
+ *
221
+ * @example
222
+ * ```typescript
223
+ * roomManager.clear();
224
+ * console.log('All rooms cleared');
225
+ * ```
226
+ */
227
+ public clear(): void {
228
+ this.rooms.clear();
229
+ this.subscribedRooms.clear();
230
+ }
231
+
232
+ /**
233
+ * Destroys the room manager and cleans up resources.
234
+ * Clears all rooms and removes all event listeners.
235
+ * After destruction, the manager cannot be reused.
236
+ *
237
+ * @example
238
+ * ```typescript
239
+ * roomManager.destroy();
240
+ * console.log('Room manager destroyed');
241
+ * ```
242
+ */
243
+ public destroy(): void {
244
+ this.logger.info("RoomManager: Destroying");
245
+ this.clear();
246
+ this.removeAllListeners();
247
+ }
248
+ }