@ynhcj/xiaoyi-channel 1.0.5 โ†’ 1.0.7

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.
@@ -10,6 +10,11 @@ export declare function setClientRuntime(rt: RuntimeEnv | undefined): void;
10
10
  * Reuses existing managers if config matches.
11
11
  */
12
12
  export declare function getXYWebSocketManager(config: XYChannelConfig): XYWebSocketManager;
13
+ /**
14
+ * Remove a specific WebSocket manager from cache.
15
+ * Disconnects the manager and removes it from the cache.
16
+ */
17
+ export declare function removeXYWebSocketManager(config: XYChannelConfig): void;
13
18
  /**
14
19
  * Clear all cached WebSocket managers.
15
20
  */
@@ -34,6 +34,23 @@ export function getXYWebSocketManager(config) {
34
34
  log(`[WS-MANAGER-CACHE] ๐Ÿ“Š Total managers after creation: ${wsManagerCache.size}`);
35
35
  return cached;
36
36
  }
37
+ /**
38
+ * Remove a specific WebSocket manager from cache.
39
+ * Disconnects the manager and removes it from the cache.
40
+ */
41
+ export function removeXYWebSocketManager(config) {
42
+ const cacheKey = `${config.apiKey}-${config.agentId}`;
43
+ const manager = wsManagerCache.get(cacheKey);
44
+ if (manager) {
45
+ console.log(`๐Ÿ—‘๏ธ [WS-MANAGER-CACHE] Removing manager from cache: ${cacheKey}`);
46
+ manager.disconnect();
47
+ wsManagerCache.delete(cacheKey);
48
+ console.log(`๐Ÿ—‘๏ธ [WS-MANAGER-CACHE] Manager removed, remaining managers: ${wsManagerCache.size}`);
49
+ }
50
+ else {
51
+ console.log(`โš ๏ธ [WS-MANAGER-CACHE] Manager not found in cache: ${cacheKey}`);
52
+ }
53
+ }
37
54
  /**
38
55
  * Clear all cached WebSocket managers.
39
56
  */
@@ -56,77 +73,75 @@ export function getCachedManagerCount() {
56
73
  * Helps identify connection issues and orphan connections.
57
74
  */
58
75
  export function diagnoseAllManagers() {
59
- const log = runtime?.log ?? console.log;
60
- log("========================================");
61
- log("๐Ÿ“Š WebSocket Manager Global Diagnostics");
62
- log("========================================");
63
- log(`Total cached managers: ${wsManagerCache.size}`);
64
- log("");
76
+ console.log("========================================");
77
+ console.log("๐Ÿ“Š WebSocket Manager Global Diagnostics");
78
+ console.log("========================================");
79
+ console.log(`Total cached managers: ${wsManagerCache.size}`);
80
+ console.log("");
65
81
  if (wsManagerCache.size === 0) {
66
- log("โ„น๏ธ No managers in cache");
67
- log("========================================");
82
+ console.log("โ„น๏ธ No managers in cache");
83
+ console.log("========================================");
68
84
  return;
69
85
  }
70
86
  let orphanCount = 0;
71
87
  wsManagerCache.forEach((manager, key) => {
72
88
  const diag = manager.getConnectionDiagnostics();
73
- log(`๐Ÿ“Œ Manager: ${key}`);
74
- log(` Shutting down: ${diag.isShuttingDown}`);
75
- log(` Total event listeners on manager: ${diag.totalEventListeners}`);
89
+ console.log(`๐Ÿ“Œ Manager: ${key}`);
90
+ console.log(` Shutting down: ${diag.isShuttingDown}`);
91
+ console.log(` Total event listeners on manager: ${diag.totalEventListeners}`);
76
92
  // Server 1
77
- log(` ๐Ÿ”Œ Server1:`);
78
- log(` - Exists: ${diag.server1.exists}`);
79
- log(` - ReadyState: ${diag.server1.readyState}`);
80
- log(` - State connected/ready: ${diag.server1.stateConnected}/${diag.server1.stateReady}`);
81
- log(` - Reconnect attempts: ${diag.server1.reconnectAttempts}`);
82
- log(` - Listeners on WebSocket: ${diag.server1.listenerCount}`);
83
- log(` - Heartbeat active: ${diag.server1.heartbeatActive}`);
84
- log(` - Has reconnect timer: ${diag.server1.hasReconnectTimer}`);
93
+ console.log(` ๐Ÿ”Œ Server1:`);
94
+ console.log(` - Exists: ${diag.server1.exists}`);
95
+ console.log(` - ReadyState: ${diag.server1.readyState}`);
96
+ console.log(` - State connected/ready: ${diag.server1.stateConnected}/${diag.server1.stateReady}`);
97
+ console.log(` - Reconnect attempts: ${diag.server1.reconnectAttempts}`);
98
+ console.log(` - Listeners on WebSocket: ${diag.server1.listenerCount}`);
99
+ console.log(` - Heartbeat active: ${diag.server1.heartbeatActive}`);
100
+ console.log(` - Has reconnect timer: ${diag.server1.hasReconnectTimer}`);
85
101
  if (diag.server1.isOrphan) {
86
- log(` โš ๏ธ ORPHAN CONNECTION DETECTED!`);
102
+ console.log(` โš ๏ธ ORPHAN CONNECTION DETECTED!`);
87
103
  orphanCount++;
88
104
  }
89
105
  // Server 2
90
- log(` ๐Ÿ”Œ Server2:`);
91
- log(` - Exists: ${diag.server2.exists}`);
92
- log(` - ReadyState: ${diag.server2.readyState}`);
93
- log(` - State connected/ready: ${diag.server2.stateConnected}/${diag.server2.stateReady}`);
94
- log(` - Reconnect attempts: ${diag.server2.reconnectAttempts}`);
95
- log(` - Listeners on WebSocket: ${diag.server2.listenerCount}`);
96
- log(` - Heartbeat active: ${diag.server2.heartbeatActive}`);
97
- log(` - Has reconnect timer: ${diag.server2.hasReconnectTimer}`);
106
+ console.log(` ๐Ÿ”Œ Server2:`);
107
+ console.log(` - Exists: ${diag.server2.exists}`);
108
+ console.log(` - ReadyState: ${diag.server2.readyState}`);
109
+ console.log(` - State connected/ready: ${diag.server2.stateConnected}/${diag.server2.stateReady}`);
110
+ console.log(` - Reconnect attempts: ${diag.server2.reconnectAttempts}`);
111
+ console.log(` - Listeners on WebSocket: ${diag.server2.listenerCount}`);
112
+ console.log(` - Heartbeat active: ${diag.server2.heartbeatActive}`);
113
+ console.log(` - Has reconnect timer: ${diag.server2.hasReconnectTimer}`);
98
114
  if (diag.server2.isOrphan) {
99
- log(` โš ๏ธ ORPHAN CONNECTION DETECTED!`);
115
+ console.log(` โš ๏ธ ORPHAN CONNECTION DETECTED!`);
100
116
  orphanCount++;
101
117
  }
102
- log("");
118
+ console.log("");
103
119
  });
104
120
  if (orphanCount > 0) {
105
- log(`โš ๏ธ Total orphan connections found: ${orphanCount}`);
106
- log(`๐Ÿ’ก Suggestion: These connections should be cleaned up`);
121
+ console.log(`โš ๏ธ Total orphan connections found: ${orphanCount}`);
122
+ console.log(`๐Ÿ’ก Suggestion: These connections should be cleaned up`);
107
123
  }
108
124
  else {
109
- log(`โœ… No orphan connections found`);
125
+ console.log(`โœ… No orphan connections found`);
110
126
  }
111
- log("========================================");
127
+ console.log("========================================");
112
128
  }
113
129
  /**
114
130
  * Clean up orphan connections across all managers.
115
131
  * Returns the number of managers that had orphan connections.
116
132
  */
117
133
  export function cleanupOrphanConnections() {
118
- const log = runtime?.log ?? console.log;
119
134
  let cleanedCount = 0;
120
135
  wsManagerCache.forEach((manager, key) => {
121
136
  const diag = manager.getConnectionDiagnostics();
122
137
  if (diag.server1.isOrphan || diag.server2.isOrphan) {
123
- log(`๐Ÿงน Cleaning up orphan connections in manager: ${key}`);
138
+ console.log(`๐Ÿงน Cleaning up orphan connections in manager: ${key}`);
124
139
  manager.disconnect();
125
140
  cleanedCount++;
126
141
  }
127
142
  });
128
143
  if (cleanedCount > 0) {
129
- log(`๐Ÿงน Cleaned up ${cleanedCount} manager(s) with orphan connections`);
144
+ console.log(`๐Ÿงน Cleaned up ${cleanedCount} manager(s) with orphan connections`);
130
145
  }
131
146
  return cleanedCount;
132
147
  }
@@ -13,12 +13,13 @@ export declare class HeartbeatManager {
13
13
  private config;
14
14
  private onTimeout;
15
15
  private serverName;
16
+ private onHeartbeatSuccess?;
16
17
  private intervalTimer;
17
18
  private timeoutTimer;
18
19
  private lastPongTime;
19
20
  private log;
20
21
  private error;
21
- constructor(ws: WebSocket, config: HeartbeatConfig, onTimeout: () => void, serverName?: string, logFn?: (msg: string, ...args: any[]) => void, errorFn?: (msg: string, ...args: any[]) => void);
22
+ constructor(ws: WebSocket, config: HeartbeatConfig, onTimeout: () => void, serverName?: string, logFn?: (msg: string, ...args: any[]) => void, errorFn?: (msg: string, ...args: any[]) => void, onHeartbeatSuccess?: () => void);
22
23
  /**
23
24
  * Start heartbeat monitoring.
24
25
  */
@@ -9,17 +9,20 @@ export class HeartbeatManager {
9
9
  config;
10
10
  onTimeout;
11
11
  serverName;
12
+ onHeartbeatSuccess;
12
13
  intervalTimer = null;
13
14
  timeoutTimer = null;
14
15
  lastPongTime = 0;
15
16
  // Logging functions following feishu pattern
16
17
  log;
17
18
  error;
18
- constructor(ws, config, onTimeout, serverName = "unknown", logFn, errorFn) {
19
+ constructor(ws, config, onTimeout, serverName = "unknown", logFn, errorFn, onHeartbeatSuccess // โœ… ๆ–ฐๅขž๏ผšๅฟƒ่ทณๆˆๅŠŸๅ›ž่ฐƒ
20
+ ) {
19
21
  this.ws = ws;
20
22
  this.config = config;
21
23
  this.onTimeout = onTimeout;
22
24
  this.serverName = serverName;
25
+ this.onHeartbeatSuccess = onHeartbeatSuccess;
23
26
  this.log = logFn ?? console.log;
24
27
  this.error = errorFn ?? console.error;
25
28
  }
@@ -36,6 +39,8 @@ export class HeartbeatManager {
36
39
  clearTimeout(this.timeoutTimer);
37
40
  this.timeoutTimer = null;
38
41
  }
42
+ // โœ… Report health: heartbeat successful
43
+ this.onHeartbeatSuccess?.();
39
44
  });
40
45
  // Start interval timer
41
46
  this.intervalTimer = setInterval(() => {
@@ -4,6 +4,11 @@ export type MonitorXYOpts = {
4
4
  runtime?: RuntimeEnv;
5
5
  abortSignal?: AbortSignal;
6
6
  accountId?: string;
7
+ setStatus?: (status: {
8
+ lastEventAt?: number;
9
+ lastInboundAt?: number;
10
+ connected?: boolean;
11
+ }) => void;
7
12
  };
8
13
  /**
9
14
  * Monitor XY channel WebSocket connections.
@@ -1,5 +1,5 @@
1
1
  import { resolveXYConfig } from "./config.js";
2
- import { getXYWebSocketManager, diagnoseAllManagers, cleanupOrphanConnections } from "./client.js";
2
+ import { getXYWebSocketManager, diagnoseAllManagers, cleanupOrphanConnections, removeXYWebSocketManager } from "./client.js";
3
3
  import { handleXYMessage } from "./bot.js";
4
4
  /**
5
5
  * Per-session serial queue that ensures messages from the same session are processed
@@ -37,11 +37,21 @@ export async function monitorXYProvider(opts = {}) {
37
37
  throw new Error(`XY account is disabled`);
38
38
  }
39
39
  const accountId = opts.accountId ?? "default";
40
+ // Create trackEvent function to report health to OpenClaw framework
41
+ const trackEvent = opts.setStatus
42
+ ? () => {
43
+ opts.setStatus({ lastEventAt: Date.now(), lastInboundAt: Date.now() });
44
+ }
45
+ : undefined;
40
46
  // ๐Ÿ” Diagnose WebSocket managers before gateway start
41
- log("๐Ÿ” [DIAGNOSTICS] Checking WebSocket managers before gateway start...");
47
+ console.log("๐Ÿ” [DIAGNOSTICS] Checking WebSocket managers before gateway start...");
42
48
  diagnoseAllManagers();
43
49
  // Get WebSocket manager (cached)
44
50
  const wsManager = getXYWebSocketManager(account);
51
+ // โœ… Set health event callback for heartbeat reporting
52
+ if (trackEvent) {
53
+ wsManager.setHealthEventCallback(trackEvent);
54
+ }
45
55
  // Track logged servers to avoid duplicate logs
46
56
  const loggedServers = new Set();
47
57
  // Track active message processing to detect duplicates
@@ -55,6 +65,8 @@ export async function monitorXYProvider(opts = {}) {
55
65
  const messageHandler = (message, sessionId, serverId) => {
56
66
  const messageKey = `${sessionId}::${message.id}`;
57
67
  log(`[MONITOR-HANDLER] ####### messageHandler triggered: serverId=${serverId}, sessionId=${sessionId}, messageId=${message.id} #######`);
68
+ // โœ… Report health: received a message
69
+ trackEvent?.();
58
70
  // Check for duplicate message handling
59
71
  if (activeMessages.has(messageKey)) {
60
72
  error(`[MONITOR-HANDLER] โš ๏ธ WARNING: Duplicate message detected! messageKey=${messageKey}, this may cause duplicate dispatchers!`);
@@ -93,10 +105,17 @@ export async function monitorXYProvider(opts = {}) {
93
105
  log(`XY gateway: ${serverId} connected`);
94
106
  loggedServers.add(serverId);
95
107
  }
108
+ // โœ… Report health: connection established
109
+ trackEvent?.();
110
+ opts.setStatus?.({ connected: true });
96
111
  };
97
112
  const disconnectedHandler = (serverId) => {
98
113
  console.warn(`XY gateway: ${serverId} disconnected`);
99
114
  loggedServers.delete(serverId);
115
+ // โœ… Report disconnection status (only if all servers disconnected)
116
+ if (loggedServers.size === 0) {
117
+ opts.setStatus?.({ connected: false });
118
+ }
100
119
  };
101
120
  const errorHandler = (err, serverId) => {
102
121
  error(`XY gateway: ${serverId} error: ${String(err)}`);
@@ -104,13 +123,13 @@ export async function monitorXYProvider(opts = {}) {
104
123
  const cleanup = () => {
105
124
  log("XY gateway: cleaning up...");
106
125
  // ๐Ÿ” Diagnose before cleanup
107
- log("๐Ÿ” [DIAGNOSTICS] Checking WebSocket managers before cleanup...");
126
+ console.log("๐Ÿ” [DIAGNOSTICS] Checking WebSocket managers before cleanup...");
108
127
  diagnoseAllManagers();
109
128
  // Stop health check interval
110
129
  if (healthCheckInterval) {
111
130
  clearInterval(healthCheckInterval);
112
131
  healthCheckInterval = null;
113
- log("โธ๏ธ Stopped periodic health check");
132
+ console.log("โธ๏ธ Stopped periodic health check");
114
133
  }
115
134
  // Remove event handlers to prevent duplicate calls on gateway restart
116
135
  wsManager.off("message", messageHandler);
@@ -120,11 +139,13 @@ export async function monitorXYProvider(opts = {}) {
120
139
  // โœ… Disconnect the wsManager to prevent connection leaks
121
140
  // This is safe because each gateway lifecycle should have clean connections
122
141
  wsManager.disconnect();
142
+ // โœ… Remove manager from cache to prevent reusing dirty state
143
+ removeXYWebSocketManager(account);
123
144
  loggedServers.clear();
124
145
  activeMessages.clear();
125
146
  log(`[MONITOR-HANDLER] ๐Ÿงน Cleanup complete, cleared active messages`);
126
147
  // ๐Ÿ” Diagnose after cleanup
127
- log("๐Ÿ” [DIAGNOSTICS] Checking WebSocket managers after cleanup...");
148
+ console.log("๐Ÿ” [DIAGNOSTICS] Checking WebSocket managers after cleanup...");
128
149
  diagnoseAllManagers();
129
150
  };
130
151
  const handleAbort = () => {
@@ -145,14 +166,14 @@ export async function monitorXYProvider(opts = {}) {
145
166
  wsManager.on("disconnected", disconnectedHandler);
146
167
  wsManager.on("error", errorHandler);
147
168
  // Start periodic health check (every 5 minutes)
148
- log("๐Ÿฅ Starting periodic health check (every 5 minutes)...");
169
+ console.log("๐Ÿฅ Starting periodic health check (every 5 minutes)...");
149
170
  healthCheckInterval = setInterval(() => {
150
- log("๐Ÿฅ [HEALTH CHECK] Periodic WebSocket diagnostics...");
171
+ console.log("๐Ÿฅ [HEALTH CHECK] Periodic WebSocket diagnostics...");
151
172
  diagnoseAllManagers();
152
173
  // Auto-cleanup orphan connections
153
174
  const cleaned = cleanupOrphanConnections();
154
175
  if (cleaned > 0) {
155
- log(`๐Ÿงน [HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
176
+ console.log(`๐Ÿงน [HEALTH CHECK] Auto-cleaned ${cleaned} manager(s) with orphan connections`);
156
177
  }
157
178
  }, 5 * 60 * 1000); // 5 minutes
158
179
  // Connect to WebSocket servers
@@ -4,6 +4,39 @@ import { XYPushService } from "./push.js";
4
4
  import { getLatestSessionContext } from "./tools/session-manager.js";
5
5
  // Special marker for default push delivery when no target is specified
6
6
  const DEFAULT_PUSH_MARKER = "default";
7
+ // File extension to MIME type mapping
8
+ const FILE_TYPE_TO_MIME_TYPE = {
9
+ txt: "text/plain",
10
+ html: "text/html",
11
+ css: "text/css",
12
+ js: "application/javascript",
13
+ json: "application/json",
14
+ png: "image/png",
15
+ jpeg: "image/jpeg",
16
+ jpg: "image/jpeg",
17
+ gif: "image/gif",
18
+ svg: "image/svg+xml",
19
+ pdf: "application/pdf",
20
+ zip: "application/zip",
21
+ doc: "application/msword",
22
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
23
+ xls: "application/vnd.ms-excel",
24
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
25
+ ppt: "application/vnd.ms-powerpoint",
26
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
27
+ mp3: "audio/mpeg",
28
+ mp4: "video/mp4",
29
+ };
30
+ /**
31
+ * Get MIME type from file extension
32
+ */
33
+ function getMimeTypeFromFilename(filename) {
34
+ const extension = filename.split(".").pop()?.toLowerCase();
35
+ if (extension && FILE_TYPE_TO_MIME_TYPE[extension]) {
36
+ return FILE_TYPE_TO_MIME_TYPE[extension];
37
+ }
38
+ return "text/plain"; // Default fallback
39
+ }
7
40
  /**
8
41
  * Outbound adapter for sending messages from OpenClaw to XY.
9
42
  * Uses Push service for direct message delivery.
@@ -128,7 +161,7 @@ export const xyOutbound = {
128
161
  // Get filename and mime type from mediaUrl
129
162
  // mediaUrl may be a local file path or URL
130
163
  const fileName = mediaUrl.split("/").pop() || "unknown";
131
- const mimeType = "text/plain";
164
+ const mimeType = getMimeTypeFromFilename(fileName);
132
165
  // Build agent_response message
133
166
  const agentResponse = {
134
167
  msgType: "agent_response",
@@ -52,7 +52,12 @@ export declare class XYWebSocketManager extends EventEmitter {
52
52
  private isShuttingDown;
53
53
  private log;
54
54
  private error;
55
+ private onHealthEvent?;
55
56
  constructor(config: XYChannelConfig, runtime?: RuntimeEnv);
57
+ /**
58
+ * Set health event callback to report activity to OpenClaw framework.
59
+ */
60
+ setHealthEventCallback(callback: () => void): void;
56
61
  /**
57
62
  * Check if config matches the current instance.
58
63
  */
@@ -41,6 +41,8 @@ export class XYWebSocketManager extends EventEmitter {
41
41
  // Logging functions following feishu pattern
42
42
  log;
43
43
  error;
44
+ // Health event callback
45
+ onHealthEvent;
44
46
  constructor(config, runtime) {
45
47
  super();
46
48
  this.config = config;
@@ -48,6 +50,12 @@ export class XYWebSocketManager extends EventEmitter {
48
50
  this.log = runtime?.log ?? console.log;
49
51
  this.error = runtime?.error ?? console.error;
50
52
  }
53
+ /**
54
+ * Set health event callback to report activity to OpenClaw framework.
55
+ */
56
+ setHealthEventCallback(callback) {
57
+ this.onHealthEvent = callback;
58
+ }
51
59
  /**
52
60
  * Check if config matches the current instance.
53
61
  */
@@ -218,14 +226,23 @@ export class XYWebSocketManager extends EventEmitter {
218
226
  */
219
227
  async connectServer(serverId, url) {
220
228
  return new Promise((resolve, reject) => {
221
- const ws = new WebSocket(url, {
229
+ // Check if URL is wss with IP address to bypass certificate validation
230
+ const urlObj = new URL(url);
231
+ const isWssWithIP = urlObj.protocol === 'wss:' && /^(\d{1,3}\.){3}\d{1,3}$/.test(urlObj.hostname);
232
+ const wsOptions = {
222
233
  headers: {
223
234
  "x-uid": this.config.uid,
224
235
  "x-api-key": this.config.apiKey,
225
236
  "x-agent-id": this.config.agentId,
226
237
  "x-request-from": "openclaw",
227
238
  },
228
- });
239
+ };
240
+ // Bypass certificate validation for wss with IP address
241
+ if (isWssWithIP) {
242
+ this.log(`${serverId}: Bypassing certificate validation for IP address: ${urlObj.hostname}`);
243
+ wsOptions.rejectUnauthorized = false;
244
+ }
245
+ const ws = new WebSocket(url, wsOptions);
229
246
  const state = serverId === "server1" ? this.state1 : this.state2;
230
247
  // Set the WebSocket instance
231
248
  if (serverId === "server1") {
@@ -340,7 +357,8 @@ export class XYWebSocketManager extends EventEmitter {
340
357
  }, () => {
341
358
  this.error(`Heartbeat timeout for ${serverId}, reconnecting...`);
342
359
  this.reconnectServer(serverId);
343
- }, serverId, this.log, this.error);
360
+ }, serverId, this.log, this.error, this.onHealthEvent // โœ… Pass health event callback
361
+ );
344
362
  heartbeat.start();
345
363
  if (serverId === "server1") {
346
364
  this.heartbeat1 = heartbeat;
@@ -416,9 +434,12 @@ export class XYWebSocketManager extends EventEmitter {
416
434
  // Wrapped format (InboundWebSocketMessage)
417
435
  const inboundMsg = parsed;
418
436
  console.log(`[XY-${serverId}] Message type: Wrapped, msgType: ${inboundMsg.msgType}`);
419
- // Skip heartbeat responses
437
+ // Handle heartbeat responses
420
438
  if (inboundMsg.msgType === "heartbeat") {
421
- console.log(`[XY-${serverId}] Skipping ${inboundMsg.msgType} message`);
439
+ console.log(`[XY-${serverId}] Received heartbeat response`);
440
+ // โœ… Report health: application-level heartbeat received
441
+ // This prevents openclaw health-monitor from marking connection as stale
442
+ this.onHealthEvent?.();
422
443
  return;
423
444
  }
424
445
  // Handle data messages (e.g., intent execution results)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ynhcj/xiaoyi-channel",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "OpenClaw Xiaoyi Channel plugin - Xiaoyi A2A protocol integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",