twindex-openclaw-plugin 0.4.2 → 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 +1 -1
- package/src/client.ts +1 -0
- package/src/index.ts +38 -15
- package/src/poll-service.ts +122 -0
package/package.json
CHANGED
package/src/client.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,28 +1,49 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { homedir } from "os";
|
|
1
4
|
import * as twindex from "./client.js";
|
|
2
|
-
import { createNotificationService } from "./
|
|
5
|
+
import { createNotificationService } from "./poll-service.js";
|
|
6
|
+
|
|
7
|
+
let bootstrapping = false;
|
|
3
8
|
|
|
4
9
|
export default function register(api: any) {
|
|
5
10
|
const cfg = () => api.config?.plugins?.entries?.twindex?.config ?? {};
|
|
6
11
|
|
|
7
12
|
function persistConfig(updates: Record<string, any>) {
|
|
8
13
|
if (!api.config?.plugins?.entries?.twindex) return;
|
|
14
|
+
// Update in-memory config
|
|
9
15
|
api.config.plugins.entries.twindex.config = {
|
|
10
16
|
...cfg(),
|
|
11
17
|
...updates,
|
|
12
18
|
};
|
|
13
|
-
|
|
19
|
+
// Write to disk — OpenClaw plugin API does not expose config.save()
|
|
20
|
+
try {
|
|
21
|
+
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
22
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
23
|
+
const disk = JSON.parse(raw);
|
|
24
|
+
if (!disk.plugins) disk.plugins = {};
|
|
25
|
+
if (!disk.plugins.entries) disk.plugins.entries = {};
|
|
26
|
+
if (!disk.plugins.entries.twindex) disk.plugins.entries.twindex = {};
|
|
27
|
+
disk.plugins.entries.twindex.config = api.config.plugins.entries.twindex.config;
|
|
28
|
+
writeFileSync(configPath, JSON.stringify(disk, null, 2) + "\n", "utf-8");
|
|
29
|
+
api.logger?.info?.("Twindex: config persisted to disk");
|
|
30
|
+
} catch (err: any) {
|
|
31
|
+
api.logger?.warn?.(`Twindex: failed to persist config: ${err.message}`);
|
|
32
|
+
}
|
|
14
33
|
}
|
|
15
34
|
|
|
16
|
-
// ──
|
|
17
|
-
const
|
|
18
|
-
api.registerService?.(
|
|
35
|
+
// ── Poll-based notification service ──────────────────────────────
|
|
36
|
+
const pollService = createNotificationService(api);
|
|
37
|
+
api.registerService?.(pollService);
|
|
19
38
|
|
|
20
39
|
// ── Auto-bootstrap: register + subscribe ────────────────────────
|
|
21
40
|
|
|
22
41
|
(async () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
if (bootstrapping) return;
|
|
43
|
+
bootstrapping = true;
|
|
44
|
+
try {
|
|
45
|
+
const config = cfg();
|
|
46
|
+
if (config.artists?.length > 0 && config.frequency && !config.apiKey) {
|
|
26
47
|
const agentId =
|
|
27
48
|
api.agentId ?? api.config?.agentId ?? `openclaw-${crypto.randomUUID()}`;
|
|
28
49
|
const reg = await twindex.register(agentId);
|
|
@@ -40,13 +61,15 @@ export default function register(api: any) {
|
|
|
40
61
|
}
|
|
41
62
|
|
|
42
63
|
persistConfig({ apiKey });
|
|
43
|
-
|
|
64
|
+
pollService.start();
|
|
44
65
|
api.logger?.info?.(
|
|
45
|
-
"Twindex: auto-bootstrap complete. Delivery via
|
|
66
|
+
"Twindex: auto-bootstrap complete. Delivery via polling.",
|
|
46
67
|
);
|
|
47
|
-
} catch (err: any) {
|
|
48
|
-
api.logger?.warn?.(`Twindex: auto-bootstrap failed: ${err.message}`);
|
|
49
68
|
}
|
|
69
|
+
} catch (err: any) {
|
|
70
|
+
api.logger?.warn?.(`Twindex: auto-bootstrap failed: ${err.message}`);
|
|
71
|
+
} finally {
|
|
72
|
+
bootstrapping = false;
|
|
50
73
|
}
|
|
51
74
|
})();
|
|
52
75
|
|
|
@@ -55,7 +78,7 @@ export default function register(api: any) {
|
|
|
55
78
|
api.registerTool({
|
|
56
79
|
name: "twindex_setup",
|
|
57
80
|
description:
|
|
58
|
-
"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.",
|
|
59
82
|
parameters: {
|
|
60
83
|
type: "object",
|
|
61
84
|
properties: {
|
|
@@ -107,8 +130,8 @@ export default function register(api: any) {
|
|
|
107
130
|
}
|
|
108
131
|
}
|
|
109
132
|
|
|
110
|
-
|
|
111
|
-
results.push("
|
|
133
|
+
pollService.start();
|
|
134
|
+
results.push("Notification polling enabled");
|
|
112
135
|
|
|
113
136
|
return {
|
|
114
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
|
+
}
|