twindex-openclaw-plugin 0.2.0 → 0.4.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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/index.ts +55 -149
- package/src/sse-service.ts +172 -0
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "twindex",
|
|
3
3
|
"name": "Twindex",
|
|
4
4
|
"description": "Music intelligence for AI agents. Get notified about tours, merch drops, releases, and presales for your favorite artists.",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.4.0",
|
|
6
6
|
"skills": ["./skills"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import * as twindex from "./client.js";
|
|
2
|
-
|
|
3
|
-
let pendingDelivery: string[] = [];
|
|
4
|
-
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
|
2
|
+
import { createNotificationService } from "./sse-service.js";
|
|
5
3
|
|
|
6
4
|
export default function register(api: any) {
|
|
7
5
|
const cfg = () => api.config?.plugins?.entries?.twindex?.config ?? {};
|
|
@@ -12,18 +10,21 @@ export default function register(api: any) {
|
|
|
12
10
|
...cfg(),
|
|
13
11
|
...updates,
|
|
14
12
|
};
|
|
15
|
-
// OpenClaw auto-persists config mutations on the entries object.
|
|
16
|
-
// If a future version requires explicit save, call api.config.save?.() here.
|
|
17
13
|
api.config.save?.();
|
|
18
14
|
}
|
|
19
15
|
|
|
20
|
-
// ──
|
|
16
|
+
// ── SSE notification service ──────────────────────────────────────
|
|
17
|
+
const sseService = createNotificationService(api);
|
|
18
|
+
api.registerService?.(sseService);
|
|
19
|
+
|
|
20
|
+
// ── Auto-bootstrap: register + subscribe ────────────────────────
|
|
21
21
|
|
|
22
22
|
(async () => {
|
|
23
23
|
const config = cfg();
|
|
24
24
|
if (config.artists?.length > 0 && config.frequency && !config.apiKey) {
|
|
25
25
|
try {
|
|
26
|
-
const agentId =
|
|
26
|
+
const agentId =
|
|
27
|
+
api.agentId ?? api.config?.agentId ?? `openclaw-${crypto.randomUUID()}`;
|
|
27
28
|
const reg = await twindex.register(agentId);
|
|
28
29
|
const apiKey = reg.api_key;
|
|
29
30
|
|
|
@@ -32,13 +33,17 @@ export default function register(api: any) {
|
|
|
32
33
|
await twindex.subscribe(apiKey, artist);
|
|
33
34
|
api.logger?.info?.(`Twindex: auto-subscribed to ${artist}`);
|
|
34
35
|
} catch (err: any) {
|
|
35
|
-
api.logger?.warn?.(
|
|
36
|
+
api.logger?.warn?.(
|
|
37
|
+
`Twindex: failed to subscribe to ${artist}: ${err.message}`,
|
|
38
|
+
);
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
persistConfig({ apiKey });
|
|
40
|
-
|
|
41
|
-
api.logger?.info?.(
|
|
43
|
+
sseService.start();
|
|
44
|
+
api.logger?.info?.(
|
|
45
|
+
"Twindex: auto-bootstrap complete. Delivery via SSE push.",
|
|
46
|
+
);
|
|
42
47
|
} catch (err: any) {
|
|
43
48
|
api.logger?.warn?.(`Twindex: auto-bootstrap failed: ${err.message}`);
|
|
44
49
|
}
|
|
@@ -50,7 +55,7 @@ export default function register(api: any) {
|
|
|
50
55
|
api.registerTool({
|
|
51
56
|
name: "twindex_setup",
|
|
52
57
|
description:
|
|
53
|
-
"Set up Twindex music notifications. Registers with Twindex, subscribes to artists, and
|
|
58
|
+
"Set up Twindex music notifications. Registers with Twindex, subscribes to artists, and starts real-time push delivery via SSE. Call this after asking your user which artists they want.",
|
|
54
59
|
parameters: {
|
|
55
60
|
type: "object",
|
|
56
61
|
properties: {
|
|
@@ -69,21 +74,27 @@ export default function register(api: any) {
|
|
|
69
74
|
},
|
|
70
75
|
required: ["artists", "frequency"],
|
|
71
76
|
},
|
|
72
|
-
async execute(
|
|
77
|
+
async execute(
|
|
78
|
+
_id: string,
|
|
79
|
+
params: { artists: string[]; frequency: string },
|
|
80
|
+
) {
|
|
73
81
|
try {
|
|
74
82
|
const config = cfg();
|
|
75
83
|
let apiKey = config.apiKey;
|
|
76
84
|
|
|
77
85
|
if (!apiKey) {
|
|
78
|
-
const agentId =
|
|
86
|
+
const agentId =
|
|
87
|
+
api.agentId ??
|
|
88
|
+
api.config?.agentId ??
|
|
89
|
+
`openclaw-${crypto.randomUUID()}`;
|
|
79
90
|
const reg = await twindex.register(agentId);
|
|
80
91
|
apiKey = reg.api_key;
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
persistConfig({
|
|
84
95
|
apiKey,
|
|
96
|
+
artists: params.artists,
|
|
85
97
|
frequency: params.frequency,
|
|
86
|
-
intervalMinutes: frequencyToMinutes(params.frequency),
|
|
87
98
|
});
|
|
88
99
|
|
|
89
100
|
const results: string[] = [];
|
|
@@ -96,13 +107,8 @@ export default function register(api: any) {
|
|
|
96
107
|
}
|
|
97
108
|
}
|
|
98
109
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const intervalDesc = frequencyDescription(params.frequency);
|
|
102
|
-
results.push(`Delivery configured: ${intervalDesc}`);
|
|
103
|
-
results.push(
|
|
104
|
-
"Notifications will be delivered automatically. No cron jobs needed.",
|
|
105
|
-
);
|
|
110
|
+
sseService.start();
|
|
111
|
+
results.push("Real-time push notifications enabled (SSE)");
|
|
106
112
|
|
|
107
113
|
return {
|
|
108
114
|
content: [{ type: "text", text: results.join("\n") }],
|
|
@@ -118,7 +124,7 @@ export default function register(api: any) {
|
|
|
118
124
|
api.registerTool({
|
|
119
125
|
name: "twindex_subscribe",
|
|
120
126
|
description:
|
|
121
|
-
"Subscribe to a new artist on Twindex. The existing delivery
|
|
127
|
+
"Subscribe to a new artist on Twindex. The existing delivery schedule will automatically pick up notifications for this artist.",
|
|
122
128
|
parameters: {
|
|
123
129
|
type: "object",
|
|
124
130
|
properties: {
|
|
@@ -152,7 +158,10 @@ export default function register(api: any) {
|
|
|
152
158
|
} catch (err: any) {
|
|
153
159
|
return {
|
|
154
160
|
content: [
|
|
155
|
-
{
|
|
161
|
+
{
|
|
162
|
+
type: "text",
|
|
163
|
+
text: `Failed to subscribe to ${params.artist}: ${err.message}`,
|
|
164
|
+
},
|
|
156
165
|
],
|
|
157
166
|
};
|
|
158
167
|
}
|
|
@@ -192,7 +201,10 @@ export default function register(api: any) {
|
|
|
192
201
|
} catch (err: any) {
|
|
193
202
|
return {
|
|
194
203
|
content: [
|
|
195
|
-
{
|
|
204
|
+
{
|
|
205
|
+
type: "text",
|
|
206
|
+
text: `Failed to unsubscribe from ${params.artist}: ${err.message}`,
|
|
207
|
+
},
|
|
196
208
|
],
|
|
197
209
|
};
|
|
198
210
|
}
|
|
@@ -208,19 +220,6 @@ export default function register(api: any) {
|
|
|
208
220
|
properties: {},
|
|
209
221
|
},
|
|
210
222
|
async execute() {
|
|
211
|
-
// Drain the in-process buffer first (avoids double-delivery with poll service)
|
|
212
|
-
if (pendingDelivery.length > 0) {
|
|
213
|
-
const buffered = pendingDelivery.splice(0);
|
|
214
|
-
return {
|
|
215
|
-
content: [
|
|
216
|
-
{
|
|
217
|
-
type: "text",
|
|
218
|
-
text: `${buffered.length} update(s):\n${buffered.join("\n")}`,
|
|
219
|
-
},
|
|
220
|
-
],
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
223
|
const apiKey = cfg().apiKey;
|
|
225
224
|
if (!apiKey) {
|
|
226
225
|
return {
|
|
@@ -234,13 +233,14 @@ export default function register(api: any) {
|
|
|
234
233
|
const notifications = await twindex.getNotifications(apiKey);
|
|
235
234
|
if (notifications.length === 0) {
|
|
236
235
|
return {
|
|
237
|
-
content: [
|
|
238
|
-
{ type: "text", text: "No unread notifications." },
|
|
239
|
-
],
|
|
236
|
+
content: [{ type: "text", text: "No unread notifications." }],
|
|
240
237
|
};
|
|
241
238
|
}
|
|
242
239
|
|
|
243
|
-
const lines =
|
|
240
|
+
const lines = notifications.map(
|
|
241
|
+
(n) =>
|
|
242
|
+
`[${n.brand}] ${n.event_type}: ${n.summary}${n.detail_url ? ` — ${n.detail_url}` : ""}`,
|
|
243
|
+
);
|
|
244
244
|
|
|
245
245
|
const ids = notifications.map((n) => n.id);
|
|
246
246
|
await twindex.markRead(apiKey, ids);
|
|
@@ -256,7 +256,10 @@ export default function register(api: any) {
|
|
|
256
256
|
} catch (err: any) {
|
|
257
257
|
return {
|
|
258
258
|
content: [
|
|
259
|
-
{
|
|
259
|
+
{
|
|
260
|
+
type: "text",
|
|
261
|
+
text: `Failed to check notifications: ${err.message}`,
|
|
262
|
+
},
|
|
260
263
|
],
|
|
261
264
|
};
|
|
262
265
|
}
|
|
@@ -297,7 +300,10 @@ export default function register(api: any) {
|
|
|
297
300
|
} catch (err: any) {
|
|
298
301
|
return {
|
|
299
302
|
content: [
|
|
300
|
-
{
|
|
303
|
+
{
|
|
304
|
+
type: "text",
|
|
305
|
+
text: `Failed to list subscriptions: ${err.message}`,
|
|
306
|
+
},
|
|
301
307
|
],
|
|
302
308
|
};
|
|
303
309
|
}
|
|
@@ -327,7 +333,10 @@ export default function register(api: any) {
|
|
|
327
333
|
} catch (err: any) {
|
|
328
334
|
return {
|
|
329
335
|
content: [
|
|
330
|
-
{
|
|
336
|
+
{
|
|
337
|
+
type: "text",
|
|
338
|
+
text: `Failed to get artist page: ${err.message}`,
|
|
339
|
+
},
|
|
331
340
|
],
|
|
332
341
|
};
|
|
333
342
|
}
|
|
@@ -343,7 +352,8 @@ export default function register(api: any) {
|
|
|
343
352
|
properties: {
|
|
344
353
|
query: {
|
|
345
354
|
type: "string",
|
|
346
|
-
description:
|
|
355
|
+
description:
|
|
356
|
+
"Search query. Examples: 'metal bands', 'the cure', 'punk rock'.",
|
|
347
357
|
},
|
|
348
358
|
},
|
|
349
359
|
required: ["query"],
|
|
@@ -405,109 +415,5 @@ export default function register(api: any) {
|
|
|
405
415
|
}
|
|
406
416
|
},
|
|
407
417
|
});
|
|
408
|
-
|
|
409
|
-
// ── Lifecycle Hook: inject notifications into agent context ────────
|
|
410
|
-
|
|
411
|
-
api.on("before_agent_start", async () => {
|
|
412
|
-
if (pendingDelivery.length === 0) return;
|
|
413
|
-
|
|
414
|
-
const messages = pendingDelivery.splice(0);
|
|
415
|
-
return {
|
|
416
|
-
systemMessage: [
|
|
417
|
-
"TWINDEX NOTIFICATIONS (deliver these to your user):",
|
|
418
|
-
...messages,
|
|
419
|
-
].join("\n"),
|
|
420
|
-
};
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
// ── Background Service: poll for notifications ─────────────────────
|
|
424
|
-
|
|
425
|
-
api.registerService?.({
|
|
426
|
-
id: "twindex-poll",
|
|
427
|
-
start: () => {
|
|
428
|
-
const config = cfg();
|
|
429
|
-
if (config.apiKey && config.frequency) {
|
|
430
|
-
startPolling(api, config.apiKey, config.frequency);
|
|
431
|
-
}
|
|
432
|
-
},
|
|
433
|
-
stop: () => {
|
|
434
|
-
if (pollTimer) {
|
|
435
|
-
clearInterval(pollTimer);
|
|
436
|
-
pollTimer = null;
|
|
437
|
-
}
|
|
438
|
-
},
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// ── Helpers ────────────────────────────────────────────────────────────
|
|
443
|
-
|
|
444
|
-
function frequencyToMinutes(frequency: string): number {
|
|
445
|
-
switch (frequency) {
|
|
446
|
-
case "realtime":
|
|
447
|
-
return 5;
|
|
448
|
-
case "daily":
|
|
449
|
-
return 1440;
|
|
450
|
-
case "periodic":
|
|
451
|
-
default:
|
|
452
|
-
return 60;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function frequencyDescription(frequency: string): string {
|
|
457
|
-
switch (frequency) {
|
|
458
|
-
case "realtime":
|
|
459
|
-
return "checking every 5 minutes";
|
|
460
|
-
case "daily":
|
|
461
|
-
return "checking once per day";
|
|
462
|
-
case "periodic":
|
|
463
|
-
default:
|
|
464
|
-
return "checking every hour";
|
|
465
|
-
}
|
|
466
418
|
}
|
|
467
419
|
|
|
468
|
-
function formatNotifications(
|
|
469
|
-
notifications: Array<{
|
|
470
|
-
brand: string;
|
|
471
|
-
event_type: string;
|
|
472
|
-
summary: string;
|
|
473
|
-
detail_url?: string;
|
|
474
|
-
}>,
|
|
475
|
-
): string[] {
|
|
476
|
-
return notifications.map(
|
|
477
|
-
(n) =>
|
|
478
|
-
`[${n.brand}] ${n.event_type}: ${n.summary}${n.detail_url ? ` — ${n.detail_url}` : ""}`,
|
|
479
|
-
);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
function startPolling(api: any, apiKey: string, frequency: string) {
|
|
483
|
-
if (pollTimer) {
|
|
484
|
-
clearInterval(pollTimer);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
const intervalMs = frequencyToMinutes(frequency) * 60 * 1000;
|
|
488
|
-
|
|
489
|
-
pollTimer = setInterval(async () => {
|
|
490
|
-
try {
|
|
491
|
-
const notifications = await twindex.getNotifications(apiKey);
|
|
492
|
-
if (notifications.length === 0) return;
|
|
493
|
-
|
|
494
|
-
// Queue for delivery BEFORE marking read — if process crashes after
|
|
495
|
-
// markRead, notifications are lost. This way they survive in the buffer.
|
|
496
|
-
const lines = formatNotifications(notifications);
|
|
497
|
-
pendingDelivery.push(...lines);
|
|
498
|
-
|
|
499
|
-
const ids = notifications.map((n) => n.id);
|
|
500
|
-
await twindex.markRead(apiKey, ids);
|
|
501
|
-
|
|
502
|
-
api.logger?.info?.(
|
|
503
|
-
`Twindex: ${notifications.length} notification(s) queued for delivery`,
|
|
504
|
-
);
|
|
505
|
-
} catch (err: any) {
|
|
506
|
-
api.logger?.warn?.(`Twindex poll error: ${err.message}`);
|
|
507
|
-
}
|
|
508
|
-
}, intervalMs);
|
|
509
|
-
|
|
510
|
-
api.logger?.info?.(
|
|
511
|
-
`Twindex: polling every ${frequencyToMinutes(frequency)} min`,
|
|
512
|
-
);
|
|
513
|
-
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// SSE background service — replaces cron polling with real-time push.
|
|
2
|
+
// Opens persistent connection to twindex.ai/api/v1/notifications/stream,
|
|
3
|
+
// delivers notifications via `openclaw agent` CLI, marks them read.
|
|
4
|
+
|
|
5
|
+
import { execFile } from "child_process";
|
|
6
|
+
import * as twindex from "./client.js";
|
|
7
|
+
|
|
8
|
+
const BASE_URL = "https://twindex.ai";
|
|
9
|
+
const BACKOFF_STEPS = [1000, 2000, 5000, 10_000, 30_000, 60_000];
|
|
10
|
+
|
|
11
|
+
function runAgent(message: string): Promise<boolean> {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
execFile(
|
|
14
|
+
"openclaw",
|
|
15
|
+
[
|
|
16
|
+
"agent",
|
|
17
|
+
"--session-id",
|
|
18
|
+
"twindex-push",
|
|
19
|
+
"--deliver",
|
|
20
|
+
"--channel",
|
|
21
|
+
"last",
|
|
22
|
+
"-m",
|
|
23
|
+
message,
|
|
24
|
+
],
|
|
25
|
+
{ timeout: 120_000 },
|
|
26
|
+
(err) => resolve(!err),
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createNotificationService(api: any) {
|
|
32
|
+
let controller: AbortController | null = null;
|
|
33
|
+
let retryCount = 0;
|
|
34
|
+
let lastEventId: string | null = null;
|
|
35
|
+
let running = false;
|
|
36
|
+
|
|
37
|
+
function getApiKey(): string | undefined {
|
|
38
|
+
return api.config?.plugins?.entries?.twindex?.config?.apiKey;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const logger = api.logger;
|
|
42
|
+
|
|
43
|
+
async function processLine(line: string) {
|
|
44
|
+
if (line.startsWith("id: ")) {
|
|
45
|
+
lastEventId = line.slice(4).trim();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!line.startsWith("data: ")) return;
|
|
50
|
+
|
|
51
|
+
const json = line.slice(6).trim();
|
|
52
|
+
if (!json) return;
|
|
53
|
+
|
|
54
|
+
let notif: { id: string; brand: string; event_type: string; summary: string };
|
|
55
|
+
try {
|
|
56
|
+
notif = JSON.parse(json);
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
logger?.info?.(`Twindex: notification received — ${notif.brand} ${notif.event_type}`);
|
|
62
|
+
|
|
63
|
+
const message =
|
|
64
|
+
`New Twindex update for ${notif.brand} (${notif.event_type}): ${notif.summary}\n` +
|
|
65
|
+
"Share this with the user naturally. If they seem busy, keep it brief.";
|
|
66
|
+
|
|
67
|
+
const delivered = await runAgent(message);
|
|
68
|
+
if (delivered) {
|
|
69
|
+
const apiKey = getApiKey();
|
|
70
|
+
if (apiKey) {
|
|
71
|
+
try {
|
|
72
|
+
await twindex.markRead(apiKey, [notif.id]);
|
|
73
|
+
} catch {
|
|
74
|
+
// Non-fatal — notification delivered, read status will catch up
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
logger?.warn?.("Twindex: agent delivery failed, notification will replay on reconnect");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function connect() {
|
|
83
|
+
const apiKey = getApiKey();
|
|
84
|
+
if (!apiKey) return;
|
|
85
|
+
|
|
86
|
+
controller = new AbortController();
|
|
87
|
+
|
|
88
|
+
const headers: Record<string, string> = {
|
|
89
|
+
Authorization: `Bearer ${apiKey}`,
|
|
90
|
+
Accept: "text/event-stream",
|
|
91
|
+
};
|
|
92
|
+
if (lastEventId) {
|
|
93
|
+
headers["Last-Event-ID"] = lastEventId;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch(`${BASE_URL}/api/v1/notifications/stream`, {
|
|
98
|
+
headers,
|
|
99
|
+
signal: controller.signal,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!res.ok) {
|
|
103
|
+
throw new Error(`SSE connect failed: ${res.status}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!res.body) {
|
|
107
|
+
throw new Error("SSE: no response body");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Connected successfully — reset backoff
|
|
111
|
+
retryCount = 0;
|
|
112
|
+
logger?.info?.("Twindex: SSE connected");
|
|
113
|
+
|
|
114
|
+
const reader = res.body.getReader();
|
|
115
|
+
const decoder = new TextDecoder();
|
|
116
|
+
let buffer = "";
|
|
117
|
+
|
|
118
|
+
while (running) {
|
|
119
|
+
const { done, value } = await reader.read();
|
|
120
|
+
if (done) break;
|
|
121
|
+
|
|
122
|
+
buffer += decoder.decode(value, { stream: true });
|
|
123
|
+
const lines = buffer.split("\n");
|
|
124
|
+
// Keep incomplete last line in buffer
|
|
125
|
+
buffer = lines.pop() ?? "";
|
|
126
|
+
|
|
127
|
+
for (const line of lines) {
|
|
128
|
+
const trimmed = line.trim();
|
|
129
|
+
if (trimmed === "" || trimmed.startsWith(": ")) continue;
|
|
130
|
+
await processLine(trimmed);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch (err: any) {
|
|
134
|
+
if (err.name === "AbortError") return;
|
|
135
|
+
logger?.warn?.(`Twindex: SSE error — ${err.message}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Reconnect with backoff if still running
|
|
139
|
+
if (running) {
|
|
140
|
+
const delay = BACKOFF_STEPS[Math.min(retryCount, BACKOFF_STEPS.length - 1)];
|
|
141
|
+
retryCount++;
|
|
142
|
+
logger?.info?.(`Twindex: reconnecting in ${delay}ms (attempt ${retryCount})`);
|
|
143
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
144
|
+
if (running) connect();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
name: "twindex-notifications",
|
|
150
|
+
|
|
151
|
+
start() {
|
|
152
|
+
if (running) return;
|
|
153
|
+
const apiKey = getApiKey();
|
|
154
|
+
if (!apiKey) {
|
|
155
|
+
logger?.info?.("Twindex: no API key — SSE service waiting for setup");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
running = true;
|
|
159
|
+
connect();
|
|
160
|
+
logger?.info?.("Twindex: SSE notification service started");
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
stop() {
|
|
164
|
+
running = false;
|
|
165
|
+
if (controller) {
|
|
166
|
+
controller.abort();
|
|
167
|
+
controller = null;
|
|
168
|
+
}
|
|
169
|
+
logger?.info?.("Twindex: SSE notification service stopped");
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|