openclaw-overlay-plugin 0.8.6 → 0.8.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.
package/dist/index.js CHANGED
@@ -10,10 +10,11 @@ import { cmdDiscover } from './src/scripts/overlay/discover.js';
10
10
  import { cmdRequestService } from './src/scripts/services/request.js';
11
11
  import { cmdServiceQueue } from './src/scripts/services/queue.js';
12
12
  import { cmdRespondService } from './src/scripts/services/respond.js';
13
+ import { cmdConnect } from './src/scripts/messaging/connect.js';
13
14
  import { setNoExit } from './src/scripts/output.js';
14
- // Track background process for proper lifecycle management
15
- let backgroundProcess = null;
15
+ // Track background service state
16
16
  let serviceRunning = false;
17
+ let abortController = null;
17
18
  // Auto-import tracking
18
19
  let autoImportInterval = null;
19
20
  let knownTxids = new Set();
@@ -164,60 +165,42 @@ function wakeAgent(text, logger, options = {}) {
164
165
  body: JSON.stringify({ prompt: text, sessionKey })
165
166
  }).catch(() => { });
166
167
  }
167
- function startBackgroundService(config, api) {
168
- if (backgroundProcess)
168
+ async function startBackgroundService(config, api) {
169
+ if (serviceRunning)
169
170
  return;
170
171
  serviceRunning = true;
172
+ abortController = new AbortController();
171
173
  requestCleanupInterval = setInterval(() => {
172
174
  if (serviceRunning)
173
175
  wokenRequests.clear();
174
176
  }, 5 * 60 * 1000);
175
- async function spawnConnect() {
176
- if (!serviceRunning)
177
- return;
178
- const { spawn } = await import('node:child_process');
179
- const base = __dirname.endsWith('dist') ? __dirname : path.join(__dirname, 'dist');
180
- const cliPath = path.join(base, 'src', 'cli.js');
181
- const env = { ...process['env'] };
182
- env.BSV_WALLET_DIR = config.walletDir || path.join(os.homedir(), '.openclaw', 'bsv-wallet');
183
- env.OVERLAY_URL = config.overlayUrl || 'https://clawoverlay.com';
184
- env.BSV_NETWORK = config.network || env.BSV_NETWORK || 'mainnet';
185
- const proc = spawn('node', [cliPath, 'connect'], { env, stdio: ['ignore', 'pipe', 'pipe'] });
186
- backgroundProcess = proc;
187
- proc.stdout?.on('data', (data) => {
188
- const lines = data.toString().split('\n').filter(Boolean);
189
- for (const line of lines) {
190
- try {
191
- const event = JSON.parse(line);
192
- if ((event.action === 'queued-for-agent' || event.action === 'already-queued') && event.serviceId) {
193
- const rid = event.id || `${event.from}-${Date.now()}`;
194
- if (wokenRequests.has(rid))
195
- return;
196
- wokenRequests.add(rid);
197
- const wakeText = `⚡ Incoming overlay service request!\n\nService: ${event.serviceId}\nFrom: ${event.from}\nPaid: ${event.satoshisReceived || '?'} sats\n\nFulfill it now:\n1. overlay({ action: "pending-requests" })\n2. Process the request\n3. overlay({ action: "fulfill", requestId: "${event.id}", recipientKey: "${event.from}", serviceId: "${event.serviceId}", result: { ... } })`;
198
- wakeAgent(wakeText, api.logger, { sessionKey: `hook:openclaw-overlay:${rid}` });
199
- }
200
- if (event.type === 'service-response' && event.action === 'received') {
201
- const wakeText = `📬 Overlay service response received!\n\nService: ${event.serviceId}\nFrom: ${event.from}\nStatus: ${event.status}\n\nFull result:\n${JSON.stringify(event.result, null, 2)}`;
202
- wakeAgent(wakeText, api.logger, { sessionKey: `hook:openclaw-overlay:resp-${event.requestId || Date.now()}` });
203
- }
204
- }
205
- catch { }
206
- }
207
- });
208
- proc.on('exit', () => {
209
- backgroundProcess = null;
210
- if (serviceRunning)
211
- setTimeout(spawnConnect, 5000);
212
- });
213
- }
214
- spawnConnect();
177
+ applyConfigToEnv(config);
178
+ // Start the connection directly as a library call
179
+ // This bypasses the child_process detection and is more efficient
180
+ cmdConnect((event) => {
181
+ if ((event.action === 'queued-for-agent' || event.action === 'already-queued') && event.serviceId) {
182
+ const rid = event.id || `${event.from}-${Date.now()}`;
183
+ if (wokenRequests.has(rid))
184
+ return;
185
+ wokenRequests.add(rid);
186
+ const wakeText = `⚡ Incoming overlay service request!\n\nService: ${event.serviceId}\nFrom: ${event.from}\nPaid: ${event.satoshisReceived || '?'} sats\n\nFulfill it now:\n1. overlay({ action: "pending-requests" })\n2. Process the request\n3. overlay({ action: "fulfill", requestId: "${event.id}", recipientKey: "${event.from}", serviceId: "${event.serviceId}", result: { ... } })`;
187
+ wakeAgent(wakeText, api.logger, { sessionKey: `hook:openclaw-overlay:${rid}` });
188
+ }
189
+ if (event.type === 'service-response' && event.action === 'received') {
190
+ const wakeText = `📬 Overlay service response received!\n\nService: ${event.serviceId}\nFrom: ${event.from}\nStatus: ${event.status}\n\nFull result:\n${JSON.stringify(event.result, null, 2)}`;
191
+ wakeAgent(wakeText, api.logger, { sessionKey: `hook:openclaw-overlay:resp-${event.requestId || Date.now()}` });
192
+ }
193
+ }, abortController.signal).catch((err) => {
194
+ if (serviceRunning && !abortController?.signal.aborted) {
195
+ api.logger?.error?.(`[openclaw-overlay] WebSocket error: ${err.message}`);
196
+ }
197
+ });
215
198
  }
216
199
  function stopBackgroundService() {
217
200
  serviceRunning = false;
218
- if (backgroundProcess) {
219
- backgroundProcess.kill();
220
- backgroundProcess = null;
201
+ if (abortController) {
202
+ abortController.abort();
203
+ abortController = null;
221
204
  }
222
205
  if (requestCleanupInterval) {
223
206
  clearInterval(requestCleanupInterval);
@@ -285,8 +268,8 @@ export function register(api) {
285
268
  await initializeServiceSystem();
286
269
  }
287
270
  catch { }
288
- startBackgroundService(pluginConfig, api);
289
- startAutoImport(pluginConfig, api);
271
+ await startBackgroundService(pluginConfig, api);
272
+ await startAutoImport(pluginConfig, api);
290
273
  },
291
274
  stop: () => stopBackgroundService()
292
275
  });
@@ -3,6 +3,6 @@
3
3
  */
4
4
  /**
5
5
  * Connect command: establish WebSocket connection for real-time messaging.
6
- * Note: This function never returns normally - it runs until SIGINT/SIGTERM.
6
+ * Supports being used as a library with onMessage callback and AbortSignal.
7
7
  */
8
- export declare function cmdConnect(): Promise<void>;
8
+ export declare function cmdConnect(onMessage?: (data: any) => void, signal?: AbortSignal): Promise<void>;
@@ -9,9 +9,9 @@ import { processMessage } from './handlers.js';
9
9
  import { ensureStateDir } from '../utils/storage.js';
10
10
  /**
11
11
  * Connect command: establish WebSocket connection for real-time messaging.
12
- * Note: This function never returns normally - it runs until SIGINT/SIGTERM.
12
+ * Supports being used as a library with onMessage callback and AbortSignal.
13
13
  */
14
- export async function cmdConnect() {
14
+ export async function cmdConnect(onMessage, signal) {
15
15
  let WebSocketClient;
16
16
  try {
17
17
  const ws = await import('ws');
@@ -33,24 +33,48 @@ export async function cmdConnect() {
33
33
  }
34
34
  catch { }
35
35
  }
36
- process.exit(0);
36
+ // Only exit if we're not running as a library
37
+ if (!onMessage) {
38
+ process.exit(0);
39
+ }
40
+ }
41
+ if (!onMessage) {
42
+ process.on('SIGINT', shutdown);
43
+ process.on('SIGTERM', shutdown);
44
+ }
45
+ if (signal) {
46
+ signal.addEventListener('abort', () => {
47
+ shouldReconnect = false;
48
+ if (currentWs) {
49
+ try {
50
+ currentWs.close();
51
+ }
52
+ catch { }
53
+ }
54
+ });
37
55
  }
38
- process.on('SIGINT', shutdown);
39
- process.on('SIGTERM', shutdown);
40
56
  function connect() {
57
+ if (signal?.aborted)
58
+ return;
41
59
  const ws = new WebSocketClient(wsUrl);
42
60
  currentWs = ws;
43
61
  ws.on('open', () => {
44
62
  reconnectDelay = 1000; // reset on successful connect
45
- console.error(JSON.stringify({ event: 'connected', identity: identityKey, overlay: OVERLAY_URL }));
63
+ const log = { event: 'connected', identity: identityKey, overlay: OVERLAY_URL };
64
+ if (onMessage)
65
+ onMessage(log);
66
+ else
67
+ console.error(JSON.stringify(log));
46
68
  });
47
69
  ws.on('message', async (data) => {
48
70
  try {
49
71
  const envelope = JSON.parse(data.toString());
50
72
  if (envelope.type === 'message') {
51
73
  const result = await processMessage(envelope.message, identityKey, privKey);
52
- // Output the result as a JSON line to stdout
53
- console.log(JSON.stringify(result));
74
+ if (onMessage)
75
+ onMessage(result);
76
+ else
77
+ console.log(JSON.stringify(result));
54
78
  // Also append to notification log
55
79
  ensureStateDir();
56
80
  try {
@@ -67,7 +91,11 @@ export async function cmdConnect() {
67
91
  });
68
92
  }
69
93
  catch (ackErr) {
70
- console.error(JSON.stringify({ event: 'ack-error', id: result.id, message: String(ackErr) }));
94
+ const log = { event: 'ack-error', id: result.id, message: String(ackErr) };
95
+ if (onMessage)
96
+ onMessage(log);
97
+ else
98
+ console.error(JSON.stringify(log));
71
99
  }
72
100
  }
73
101
  }
@@ -84,7 +112,10 @@ export async function cmdConnect() {
84
112
  txid: envelope.txid,
85
113
  _ts: Date.now(),
86
114
  };
87
- console.log(JSON.stringify(announcement));
115
+ if (onMessage)
116
+ onMessage(announcement);
117
+ else
118
+ console.log(JSON.stringify(announcement));
88
119
  ensureStateDir();
89
120
  try {
90
121
  fs.appendFileSync(PATHS.notifications, JSON.stringify(announcement) + '\n');
@@ -93,22 +124,38 @@ export async function cmdConnect() {
93
124
  }
94
125
  }
95
126
  catch (err) {
96
- console.error(JSON.stringify({ event: 'process-error', message: String(err) }));
127
+ const log = { event: 'process-error', message: String(err) };
128
+ if (onMessage)
129
+ onMessage(log);
130
+ else
131
+ console.error(JSON.stringify(log));
97
132
  }
98
133
  });
99
134
  ws.on('close', () => {
100
135
  currentWs = null;
101
- if (shouldReconnect) {
102
- console.error(JSON.stringify({ event: 'disconnected', reconnectMs: reconnectDelay }));
136
+ if (shouldReconnect && !signal?.aborted) {
137
+ const log = { event: 'disconnected', reconnectMs: reconnectDelay };
138
+ if (onMessage)
139
+ onMessage(log);
140
+ else
141
+ console.error(JSON.stringify(log));
103
142
  setTimeout(connect, reconnectDelay);
104
143
  reconnectDelay = Math.min(reconnectDelay * 2, 30000);
105
144
  }
106
145
  });
107
146
  ws.on('error', (err) => {
108
- console.error(JSON.stringify({ event: 'error', message: err.message }));
147
+ const log = { event: 'error', message: err.message };
148
+ if (onMessage)
149
+ onMessage(log);
150
+ else
151
+ console.error(JSON.stringify(log));
109
152
  });
110
153
  }
111
154
  connect();
112
- // Keep the process alive — never resolves
113
- await new Promise(() => { });
155
+ // Keep alive
156
+ return new Promise((resolve) => {
157
+ if (signal) {
158
+ signal.addEventListener('abort', () => resolve());
159
+ }
160
+ });
114
161
  }
package/index.ts CHANGED
@@ -11,11 +11,12 @@ import { cmdDiscover } from './src/scripts/overlay/discover.js';
11
11
  import { cmdRequestService } from './src/scripts/services/request.js';
12
12
  import { cmdServiceQueue } from './src/scripts/services/queue.js';
13
13
  import { cmdRespondService } from './src/scripts/services/respond.js';
14
+ import { cmdConnect } from './src/scripts/messaging/connect.js';
14
15
  import { setNoExit } from './src/scripts/output.js';
15
16
 
16
- // Track background process for proper lifecycle management
17
- let backgroundProcess: any = null;
17
+ // Track background service state
18
18
  let serviceRunning = false;
19
+ let abortController: AbortController | null = null;
19
20
 
20
21
  // Auto-import tracking
21
22
  let autoImportInterval: any = null;
@@ -176,59 +177,44 @@ function wakeAgent(text: string, logger: any, options: { sessionKey?: string } =
176
177
  }).catch(() => {});
177
178
  }
178
179
 
179
- function startBackgroundService(config: any, api: any) {
180
- if (backgroundProcess) return;
180
+ async function startBackgroundService(config: any, api: any) {
181
+ if (serviceRunning) return;
181
182
  serviceRunning = true;
183
+ abortController = new AbortController();
182
184
 
183
185
  requestCleanupInterval = setInterval(() => {
184
186
  if (serviceRunning) wokenRequests.clear();
185
187
  }, 5 * 60 * 1000);
186
188
 
187
- async function spawnConnect() {
188
- if (!serviceRunning) return;
189
- const { spawn } = await import('node:child_process');
190
- const base = __dirname.endsWith('dist') ? __dirname : path.join(__dirname, 'dist');
191
- const cliPath = path.join(base, 'src', 'cli.js');
192
-
193
- const env = { ...(process as any)['env'] };
194
- env.BSV_WALLET_DIR = config.walletDir || path.join(os.homedir(), '.openclaw', 'bsv-wallet');
195
- env.OVERLAY_URL = config.overlayUrl || 'https://clawoverlay.com';
196
- env.BSV_NETWORK = config.network || env.BSV_NETWORK || 'mainnet';
197
-
198
- const proc = spawn('node', [cliPath, 'connect'], { env, stdio: ['ignore', 'pipe', 'pipe'] });
199
- backgroundProcess = proc;
200
-
201
- proc.stdout?.on('data', (data: any) => {
202
- const lines = data.toString().split('\n').filter(Boolean);
203
- for (const line of lines) {
204
- try {
205
- const event = JSON.parse(line);
206
- if ((event.action === 'queued-for-agent' || event.action === 'already-queued') && event.serviceId) {
207
- const rid = event.id || `${event.from}-${Date.now()}`;
208
- if (wokenRequests.has(rid)) return;
209
- wokenRequests.add(rid);
210
- const wakeText = `⚡ Incoming overlay service request!\n\nService: ${event.serviceId}\nFrom: ${event.from}\nPaid: ${event.satoshisReceived || '?'} sats\n\nFulfill it now:\n1. overlay({ action: "pending-requests" })\n2. Process the request\n3. overlay({ action: "fulfill", requestId: "${event.id}", recipientKey: "${event.from}", serviceId: "${event.serviceId}", result: { ... } })`;
211
- wakeAgent(wakeText, api.logger, { sessionKey: `hook:openclaw-overlay:${rid}` });
212
- }
213
- if (event.type === 'service-response' && event.action === 'received') {
214
- const wakeText = `📬 Overlay service response received!\n\nService: ${event.serviceId}\nFrom: ${event.from}\nStatus: ${event.status}\n\nFull result:\n${JSON.stringify(event.result, null, 2)}`;
215
- wakeAgent(wakeText, api.logger, { sessionKey: `hook:openclaw-overlay:resp-${event.requestId || Date.now()}` });
216
- }
217
- } catch {}
218
- }
219
- });
220
-
221
- proc.on('exit', () => {
222
- backgroundProcess = null;
223
- if (serviceRunning) setTimeout(spawnConnect, 5000);
224
- });
225
- }
226
- spawnConnect();
189
+ applyConfigToEnv(config);
190
+
191
+ // Start the connection directly as a library call
192
+ // This bypasses the child_process detection and is more efficient
193
+ cmdConnect((event: any) => {
194
+ if ((event.action === 'queued-for-agent' || event.action === 'already-queued') && event.serviceId) {
195
+ const rid = event.id || `${event.from}-${Date.now()}`;
196
+ if (wokenRequests.has(rid)) return;
197
+ wokenRequests.add(rid);
198
+ const wakeText = `⚡ Incoming overlay service request!\n\nService: ${event.serviceId}\nFrom: ${event.from}\nPaid: ${event.satoshisReceived || '?'} sats\n\nFulfill it now:\n1. overlay({ action: "pending-requests" })\n2. Process the request\n3. overlay({ action: "fulfill", requestId: "${event.id}", recipientKey: "${event.from}", serviceId: "${event.serviceId}", result: { ... } })`;
199
+ wakeAgent(wakeText, api.logger, { sessionKey: `hook:openclaw-overlay:${rid}` });
200
+ }
201
+ if (event.type === 'service-response' && event.action === 'received') {
202
+ const wakeText = `📬 Overlay service response received!\n\nService: ${event.serviceId}\nFrom: ${event.from}\nStatus: ${event.status}\n\nFull result:\n${JSON.stringify(event.result, null, 2)}`;
203
+ wakeAgent(wakeText, api.logger, { sessionKey: `hook:openclaw-overlay:resp-${event.requestId || Date.now()}` });
204
+ }
205
+ }, abortController.signal).catch((err) => {
206
+ if (serviceRunning && !abortController?.signal.aborted) {
207
+ api.logger?.error?.(`[openclaw-overlay] WebSocket error: ${err.message}`);
208
+ }
209
+ });
227
210
  }
228
211
 
229
212
  function stopBackgroundService() {
230
213
  serviceRunning = false;
231
- if (backgroundProcess) { backgroundProcess.kill(); backgroundProcess = null; }
214
+ if (abortController) {
215
+ abortController.abort();
216
+ abortController = null;
217
+ }
232
218
  if (requestCleanupInterval) { clearInterval(requestCleanupInterval); requestCleanupInterval = null; }
233
219
  wokenRequests.clear();
234
220
  if (autoImportInterval) { clearInterval(autoImportInterval); autoImportInterval = null; }
@@ -288,8 +274,8 @@ export function register(api: any) {
288
274
  id: "openclaw-overlay-relay",
289
275
  start: async () => {
290
276
  try { await initializeServiceSystem(); } catch {}
291
- startBackgroundService(pluginConfig, api);
292
- startAutoImport(pluginConfig, api);
277
+ await startBackgroundService(pluginConfig, api);
278
+ await startAutoImport(pluginConfig, api);
293
279
  },
294
280
  stop: () => stopBackgroundService()
295
281
  });
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-overlay-plugin",
3
3
  "name": "BSV Overlay Network",
4
4
  "description": "OpenClaw Overlay — decentralized agent marketplace with BSV micropayments",
5
- "version": "0.8.6",
5
+ "version": "0.8.7",
6
6
  "skills": [
7
7
  "./SKILL.md"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-overlay-plugin",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "description": "Openclaw BSV Overlay — agent discovery, service marketplace, and micropayments on the BSV blockchain",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -11,9 +11,9 @@ import { ensureStateDir } from '../utils/storage.js';
11
11
 
12
12
  /**
13
13
  * Connect command: establish WebSocket connection for real-time messaging.
14
- * Note: This function never returns normally - it runs until SIGINT/SIGTERM.
14
+ * Supports being used as a library with onMessage callback and AbortSignal.
15
15
  */
16
- export async function cmdConnect(): Promise<void> {
16
+ export async function cmdConnect(onMessage?: (data: any) => void, signal?: AbortSignal): Promise<void> {
17
17
  let WebSocketClient: any;
18
18
  try {
19
19
  const ws = await import('ws');
@@ -34,19 +34,36 @@ export async function cmdConnect(): Promise<void> {
34
34
  if (currentWs) {
35
35
  try { currentWs.close(); } catch {}
36
36
  }
37
- process.exit(0);
37
+ // Only exit if we're not running as a library
38
+ if (!onMessage) {
39
+ process.exit(0);
40
+ }
38
41
  }
39
42
 
40
- process.on('SIGINT', shutdown);
41
- process.on('SIGTERM', shutdown);
43
+ if (!onMessage) {
44
+ process.on('SIGINT', shutdown);
45
+ process.on('SIGTERM', shutdown);
46
+ }
47
+
48
+ if (signal) {
49
+ signal.addEventListener('abort', () => {
50
+ shouldReconnect = false;
51
+ if (currentWs) {
52
+ try { currentWs.close(); } catch {}
53
+ }
54
+ });
55
+ }
42
56
 
43
57
  function connect() {
58
+ if (signal?.aborted) return;
44
59
  const ws = new WebSocketClient(wsUrl);
45
60
  currentWs = ws;
46
61
 
47
62
  ws.on('open', () => {
48
63
  reconnectDelay = 1000; // reset on successful connect
49
- console.error(JSON.stringify({ event: 'connected', identity: identityKey, overlay: OVERLAY_URL }));
64
+ const log = { event: 'connected', identity: identityKey, overlay: OVERLAY_URL };
65
+ if (onMessage) onMessage(log);
66
+ else console.error(JSON.stringify(log));
50
67
  });
51
68
 
52
69
  ws.on('message', async (data: any) => {
@@ -54,8 +71,9 @@ export async function cmdConnect(): Promise<void> {
54
71
  const envelope = JSON.parse(data.toString());
55
72
  if (envelope.type === 'message') {
56
73
  const result = await processMessage(envelope.message, identityKey, privKey);
57
- // Output the result as a JSON line to stdout
58
- console.log(JSON.stringify(result));
74
+
75
+ if (onMessage) onMessage(result);
76
+ else console.log(JSON.stringify(result));
59
77
 
60
78
  // Also append to notification log
61
79
  ensureStateDir();
@@ -72,7 +90,9 @@ export async function cmdConnect(): Promise<void> {
72
90
  body: JSON.stringify({ identity: identityKey, messageIds: [result.id] }),
73
91
  });
74
92
  } catch (ackErr: any) {
75
- console.error(JSON.stringify({ event: 'ack-error', id: result.id, message: String(ackErr) }));
93
+ const log = { event: 'ack-error', id: result.id, message: String(ackErr) };
94
+ if (onMessage) onMessage(log);
95
+ else console.error(JSON.stringify(log));
76
96
  }
77
97
  }
78
98
  }
@@ -90,32 +110,45 @@ export async function cmdConnect(): Promise<void> {
90
110
  txid: envelope.txid,
91
111
  _ts: Date.now(),
92
112
  };
93
- console.log(JSON.stringify(announcement));
113
+ if (onMessage) onMessage(announcement);
114
+ else console.log(JSON.stringify(announcement));
115
+
94
116
  ensureStateDir();
95
117
  try {
96
118
  fs.appendFileSync(PATHS.notifications, JSON.stringify(announcement) + '\n');
97
119
  } catch {}
98
120
  }
99
121
  } catch (err: any) {
100
- console.error(JSON.stringify({ event: 'process-error', message: String(err) }));
122
+ const log = { event: 'process-error', message: String(err) };
123
+ if (onMessage) onMessage(log);
124
+ else console.error(JSON.stringify(log));
101
125
  }
102
126
  });
103
127
 
104
128
  ws.on('close', () => {
105
129
  currentWs = null;
106
- if (shouldReconnect) {
107
- console.error(JSON.stringify({ event: 'disconnected', reconnectMs: reconnectDelay }));
130
+ if (shouldReconnect && !signal?.aborted) {
131
+ const log = { event: 'disconnected', reconnectMs: reconnectDelay };
132
+ if (onMessage) onMessage(log);
133
+ else console.error(JSON.stringify(log));
134
+
108
135
  setTimeout(connect, reconnectDelay);
109
136
  reconnectDelay = Math.min(reconnectDelay * 2, 30000);
110
137
  }
111
138
  });
112
139
 
113
140
  ws.on('error', (err: any) => {
114
- console.error(JSON.stringify({ event: 'error', message: err.message }));
141
+ const log = { event: 'error', message: err.message };
142
+ if (onMessage) onMessage(log);
143
+ else console.error(JSON.stringify(log));
115
144
  });
116
145
  }
117
146
 
118
147
  connect();
119
- // Keep the process alive — never resolves
120
- await new Promise(() => {});
148
+ // Keep alive
149
+ return new Promise((resolve) => {
150
+ if (signal) {
151
+ signal.addEventListener('abort', () => resolve());
152
+ }
153
+ });
121
154
  }