twindex-openclaw-plugin 0.4.3 → 0.5.1
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 +1 -1
- package/src/client.ts +17 -8
- package/src/index.ts +9 -9
- package/src/poll-service.ts +122 -0
package/package.json
CHANGED
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 {
|
|
@@ -96,9 +97,11 @@ export async function subscribe(
|
|
|
96
97
|
export async function listSubscriptions(
|
|
97
98
|
apiKey: string,
|
|
98
99
|
): Promise<Subscription[]> {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
const res = await request<{ subscriptions: Subscription[] }>(
|
|
101
|
+
"/api/v1/subscriptions",
|
|
102
|
+
{ headers: authHeader(apiKey) },
|
|
103
|
+
);
|
|
104
|
+
return res.subscriptions ?? [];
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
/** Unsubscribe from an artist. */
|
|
@@ -116,9 +119,11 @@ export async function unsubscribe(
|
|
|
116
119
|
export async function getNotifications(
|
|
117
120
|
apiKey: string,
|
|
118
121
|
): Promise<Notification[]> {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
const res = await request<{ notifications: Notification[] }>(
|
|
123
|
+
"/api/v1/notifications",
|
|
124
|
+
{ headers: authHeader(apiKey) },
|
|
125
|
+
);
|
|
126
|
+
return res.notifications ?? [];
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
/** Mark notifications as read. */
|
|
@@ -135,14 +140,18 @@ export async function markRead(
|
|
|
135
140
|
|
|
136
141
|
/** Search the music index (no auth). */
|
|
137
142
|
export async function search(query: string): Promise<SearchResult[]> {
|
|
138
|
-
|
|
143
|
+
const res = await request<{ results: SearchResult[] }>(
|
|
139
144
|
`/api/v1/search?q=${encodeURIComponent(query)}`,
|
|
140
145
|
);
|
|
146
|
+
return res.results ?? [];
|
|
141
147
|
}
|
|
142
148
|
|
|
143
149
|
/** List all indexed artists (no auth). */
|
|
144
150
|
export async function listArtists(): Promise<Artist[]> {
|
|
145
|
-
|
|
151
|
+
const res = await request<{ artists: Artist[] }>(
|
|
152
|
+
"/api/v1/artists",
|
|
153
|
+
);
|
|
154
|
+
return res.artists ?? [];
|
|
146
155
|
}
|
|
147
156
|
|
|
148
157
|
/** Get a single artist page as markdown (no auth). */
|
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 "./
|
|
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
|
-
// ──
|
|
36
|
-
const
|
|
37
|
-
api.registerService?.(
|
|
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
|
-
|
|
64
|
+
pollService.start();
|
|
65
65
|
api.logger?.info?.(
|
|
66
|
-
"Twindex: auto-bootstrap complete. Delivery via
|
|
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
|
|
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
|
-
|
|
134
|
-
results.push("
|
|
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
|
+
}
|