twindex-openclaw-plugin 0.4.3 → 0.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twindex-openclaw-plugin",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "Music intelligence for AI agents. Tours, merch drops, releases, presales.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/client.ts CHANGED
@@ -22,6 +22,7 @@ interface Notification {
22
22
  twindex_url?: string;
23
23
  detail_url?: string;
24
24
  created_at: string;
25
+ read_at?: string | null;
25
26
  }
26
27
 
27
28
  interface Artist {
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync } from "fs";
2
2
  import { join } from "path";
3
3
  import { homedir } from "os";
4
4
  import * as twindex from "./client.js";
5
- import { createNotificationService } from "./sse-service.js";
5
+ import { createNotificationService } from "./poll-service.js";
6
6
 
7
7
  let bootstrapping = false;
8
8
 
@@ -32,9 +32,9 @@ export default function register(api: any) {
32
32
  }
33
33
  }
34
34
 
35
- // ── SSE notification service ──────────────────────────────────────
36
- const sseService = createNotificationService(api);
37
- api.registerService?.(sseService);
35
+ // ── Poll-based notification service ──────────────────────────────
36
+ const pollService = createNotificationService(api);
37
+ api.registerService?.(pollService);
38
38
 
39
39
  // ── Auto-bootstrap: register + subscribe ────────────────────────
40
40
 
@@ -61,9 +61,9 @@ export default function register(api: any) {
61
61
  }
62
62
 
63
63
  persistConfig({ apiKey });
64
- sseService.start();
64
+ pollService.start();
65
65
  api.logger?.info?.(
66
- "Twindex: auto-bootstrap complete. Delivery via SSE push.",
66
+ "Twindex: auto-bootstrap complete. Delivery via polling.",
67
67
  );
68
68
  }
69
69
  } catch (err: any) {
@@ -78,7 +78,7 @@ export default function register(api: any) {
78
78
  api.registerTool({
79
79
  name: "twindex_setup",
80
80
  description:
81
- "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.",
81
+ "Set up Twindex music notifications. Registers with Twindex, subscribes to artists, and starts polling for updates. Call this after asking your user which artists they want.",
82
82
  parameters: {
83
83
  type: "object",
84
84
  properties: {
@@ -130,8 +130,8 @@ export default function register(api: any) {
130
130
  }
131
131
  }
132
132
 
133
- sseService.start();
134
- results.push("Real-time push notifications enabled (SSE)");
133
+ pollService.start();
134
+ results.push("Notification polling enabled");
135
135
 
136
136
  return {
137
137
  content: [{ type: "text", text: results.join("\n") }],
@@ -0,0 +1,122 @@
1
+ // Polling notification service — replaces SSE for frameworks that can't hold
2
+ // persistent connections (like OpenClaw plugins).
3
+ // Polls GET /api/v1/notifications at configurable intervals, delivers via
4
+ // `openclaw agent` CLI, marks read after confirmed delivery.
5
+
6
+ import { execFile } from "child_process";
7
+ import * as twindex from "./client.js";
8
+
9
+ const FREQUENCY_MS: Record<string, number> = {
10
+ realtime: 5 * 60 * 1000, // 5 min
11
+ periodic: 60 * 60 * 1000, // 1 hour
12
+ daily: 24 * 60 * 60 * 1000, // 24 hours
13
+ };
14
+
15
+ function runAgent(message: string): Promise<boolean> {
16
+ return new Promise((resolve) => {
17
+ execFile(
18
+ "openclaw",
19
+ [
20
+ "agent",
21
+ "--session-id",
22
+ "twindex-push",
23
+ "--deliver",
24
+ "--channel",
25
+ "last",
26
+ "-m",
27
+ message,
28
+ ],
29
+ { timeout: 120_000 },
30
+ (err) => resolve(!err),
31
+ );
32
+ });
33
+ }
34
+
35
+ export function createNotificationService(api: any) {
36
+ let timer: ReturnType<typeof setInterval> | null = null;
37
+ let running = false;
38
+ let polling = false; // guard against overlapping polls
39
+
40
+ function getApiKey(): string | undefined {
41
+ return api.config?.plugins?.entries?.twindex?.config?.apiKey;
42
+ }
43
+
44
+ function getFrequency(): string {
45
+ return api.config?.plugins?.entries?.twindex?.config?.frequency ?? "periodic";
46
+ }
47
+
48
+ const logger = api.logger;
49
+
50
+ async function poll() {
51
+ if (polling) return;
52
+ polling = true;
53
+
54
+ try {
55
+ const apiKey = getApiKey();
56
+ if (!apiKey) return;
57
+
58
+ const notifications = await twindex.getNotifications(apiKey);
59
+ if (!notifications || notifications.length === 0) return;
60
+
61
+ // Filter to unread only
62
+ const unread = notifications.filter((n) => !n.read_at);
63
+ if (unread.length === 0) return;
64
+
65
+ logger?.info?.(`Twindex: ${unread.length} unread notification(s)`);
66
+
67
+ for (const notif of unread) {
68
+ const message =
69
+ `New Twindex update for ${notif.brand} (${notif.event_type}): ${notif.summary}\n` +
70
+ "Share this with the user naturally. If they seem busy, keep it brief.";
71
+
72
+ const delivered = await runAgent(message);
73
+ if (delivered) {
74
+ try {
75
+ await twindex.markRead(apiKey, [notif.id]);
76
+ } catch {
77
+ // Non-fatal — delivered but read status will catch up
78
+ }
79
+ } else {
80
+ logger?.warn?.("Twindex: agent delivery failed, will retry next poll");
81
+ break; // Don't pile up failed deliveries
82
+ }
83
+ }
84
+ } catch (err: any) {
85
+ logger?.warn?.(`Twindex: poll error — ${err.message}`);
86
+ } finally {
87
+ polling = false;
88
+ }
89
+ }
90
+
91
+ return {
92
+ id: "twindex-notifications",
93
+
94
+ start() {
95
+ if (running) return;
96
+ const apiKey = getApiKey();
97
+ if (!apiKey) {
98
+ logger?.info?.("Twindex: no API key — poll service waiting for setup");
99
+ return;
100
+ }
101
+
102
+ running = true;
103
+ const freq = getFrequency();
104
+ const intervalMs = FREQUENCY_MS[freq] ?? FREQUENCY_MS.periodic;
105
+
106
+ // Immediate first poll
107
+ poll();
108
+
109
+ timer = setInterval(poll, intervalMs);
110
+ logger?.info?.(`Twindex: poll service started (${freq}, every ${intervalMs / 1000}s)`);
111
+ },
112
+
113
+ stop() {
114
+ running = false;
115
+ if (timer) {
116
+ clearInterval(timer);
117
+ timer = null;
118
+ }
119
+ logger?.info?.("Twindex: poll service stopped");
120
+ },
121
+ };
122
+ }