twindex-openclaw-plugin 0.8.4 → 0.8.20260409
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 +2 -2
- package/package.json +1 -1
- package/src/index.ts +46 -36
- package/src/plugin-config.ts +106 -0
- package/src/poll-service.ts +13 -46
- package/src/sse-service.ts +4 -2
- package/src/config.ts +0 -23
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "twindex",
|
|
2
|
+
"id": "twindex-openclaw-plugin",
|
|
3
3
|
"name": "New Lore",
|
|
4
4
|
"description": "Music intelligence for AI agents. Get notified about tours, merch drops, releases, and presales for your favorite artists.",
|
|
5
|
-
"version": "0.8.
|
|
5
|
+
"version": "0.8.20260409",
|
|
6
6
|
"skills": ["./skills"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,40 +1,26 @@
|
|
|
1
|
-
import { readFileSync,
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import * as twindex from "./client.js";
|
|
5
5
|
import { createNotificationService } from "./poll-service.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getPluginConfig,
|
|
8
|
+
migrateLegacyPluginConfig,
|
|
9
|
+
persistPluginConfig,
|
|
10
|
+
} from "./plugin-config.js";
|
|
7
11
|
|
|
8
12
|
let bootstrapping = false;
|
|
9
13
|
|
|
10
14
|
export default function register(api: any) {
|
|
11
|
-
const cfg = () =>
|
|
15
|
+
const cfg = () => getPluginConfig(api);
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
...updates,
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// Update in-memory config when the plugin entry is available.
|
|
20
|
-
if (api.config?.plugins?.entries?.twindex) {
|
|
21
|
-
api.config.plugins.entries.twindex.config = nextConfig;
|
|
22
|
-
}
|
|
17
|
+
if (migrateLegacyPluginConfig(api)) {
|
|
18
|
+
api.logger?.info?.("NewLore: migrated legacy twindex plugin config");
|
|
19
|
+
}
|
|
23
20
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const raw = readFileSync(configPath, "utf-8");
|
|
28
|
-
const disk = JSON.parse(raw);
|
|
29
|
-
if (!disk.plugins) disk.plugins = {};
|
|
30
|
-
if (!disk.plugins.entries) disk.plugins.entries = {};
|
|
31
|
-
if (!disk.plugins.entries.twindex) disk.plugins.entries.twindex = {};
|
|
32
|
-
disk.plugins.entries.twindex.config = nextConfig;
|
|
33
|
-
writeFileSync(configPath, JSON.stringify(disk, null, 2) + "\n", "utf-8");
|
|
34
|
-
api.logger?.info?.("NewLore: config persisted to disk");
|
|
35
|
-
} catch (err: any) {
|
|
36
|
-
api.logger?.warn?.(`NewLore: failed to persist config: ${err.message}`);
|
|
37
|
-
}
|
|
21
|
+
function persistConfig(updates: Record<string, any>) {
|
|
22
|
+
persistPluginConfig(api, updates);
|
|
23
|
+
api.logger?.info?.("NewLore: config persisted to disk");
|
|
38
24
|
}
|
|
39
25
|
|
|
40
26
|
// ── Poll-based notification service ──────────────────────────────
|
|
@@ -74,7 +60,7 @@ export default function register(api: any) {
|
|
|
74
60
|
bootstrapping = true;
|
|
75
61
|
try {
|
|
76
62
|
const config = cfg();
|
|
77
|
-
if (config.artists?.length > 0 &&
|
|
63
|
+
if (config.artists?.length > 0 && !config.apiKey) {
|
|
78
64
|
const agentId =
|
|
79
65
|
api.agentId ?? api.config?.agentId ?? `openclaw-${crypto.randomUUID()}`;
|
|
80
66
|
const reg = await twindex.register(agentId);
|
|
@@ -92,7 +78,10 @@ export default function register(api: any) {
|
|
|
92
78
|
}
|
|
93
79
|
|
|
94
80
|
// Auto-detect chatTarget from channel config if not explicitly set
|
|
95
|
-
const updates: Record<string, any> = {
|
|
81
|
+
const updates: Record<string, any> = {
|
|
82
|
+
apiKey,
|
|
83
|
+
frequency: config.frequency ?? "periodic",
|
|
84
|
+
};
|
|
96
85
|
const chatTarget = inferChatTarget();
|
|
97
86
|
if (chatTarget) {
|
|
98
87
|
updates.chatTarget = chatTarget;
|
|
@@ -158,7 +147,7 @@ export default function register(api: any) {
|
|
|
158
147
|
chat_target: {
|
|
159
148
|
type: "string",
|
|
160
149
|
description:
|
|
161
|
-
"Delivery target. For Telegram, this is the chat ID. For Slack, the channel ID.",
|
|
150
|
+
"Optional. Delivery target. For Telegram, this is the chat ID. For Slack, the channel ID. When omitted, the plugin infers it from the active channel config.",
|
|
162
151
|
},
|
|
163
152
|
chat_channel: {
|
|
164
153
|
type: "string",
|
|
@@ -172,15 +161,35 @@ export default function register(api: any) {
|
|
|
172
161
|
"User's preferred city for tour notifications (e.g. 'san-francisco', 'new-york', 'london'). When set, tour notifications highlight shows in this city.",
|
|
173
162
|
},
|
|
174
163
|
},
|
|
175
|
-
required: ["artists"
|
|
164
|
+
required: ["artists"],
|
|
176
165
|
},
|
|
177
166
|
async execute(
|
|
178
167
|
_id: string,
|
|
179
|
-
params: {
|
|
168
|
+
params: {
|
|
169
|
+
artists: string[];
|
|
170
|
+
frequency?: string;
|
|
171
|
+
chat_target?: string;
|
|
172
|
+
chat_channel?: string;
|
|
173
|
+
city?: string;
|
|
174
|
+
},
|
|
180
175
|
) {
|
|
181
176
|
try {
|
|
182
177
|
const config = cfg();
|
|
183
178
|
let apiKey = config.apiKey;
|
|
179
|
+
const frequency = params.frequency ?? config.frequency ?? "periodic";
|
|
180
|
+
const chatTarget = params.chat_target ?? inferChatTarget();
|
|
181
|
+
const chatChannel = params.chat_channel ?? inferChatChannel();
|
|
182
|
+
|
|
183
|
+
if (!chatTarget) {
|
|
184
|
+
return {
|
|
185
|
+
content: [
|
|
186
|
+
{
|
|
187
|
+
type: "text",
|
|
188
|
+
text: "Setup needs a delivery target. Message me from Telegram or Slack first, or pass chat_target explicitly.",
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
184
193
|
|
|
185
194
|
if (!apiKey) {
|
|
186
195
|
const agentId =
|
|
@@ -194,10 +203,10 @@ export default function register(api: any) {
|
|
|
194
203
|
persistConfig({
|
|
195
204
|
apiKey,
|
|
196
205
|
artists: params.artists,
|
|
197
|
-
frequency
|
|
198
|
-
chatTarget
|
|
199
|
-
chatChannel
|
|
200
|
-
city: params.city ?? "",
|
|
206
|
+
frequency,
|
|
207
|
+
chatTarget,
|
|
208
|
+
chatChannel,
|
|
209
|
+
city: params.city ?? config.city ?? "",
|
|
201
210
|
});
|
|
202
211
|
|
|
203
212
|
if (params.city) {
|
|
@@ -220,6 +229,7 @@ export default function register(api: any) {
|
|
|
220
229
|
|
|
221
230
|
pollService.start();
|
|
222
231
|
results.push("Notification polling enabled");
|
|
232
|
+
results.push(`Delivery: ${chatChannel}:${chatTarget}`);
|
|
223
233
|
|
|
224
234
|
return {
|
|
225
235
|
content: [{ type: "text", text: results.join("\n") }],
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
|
|
5
|
+
const LEGACY_PLUGIN_IDS = ["twindex"];
|
|
6
|
+
|
|
7
|
+
export type NewLorePluginConfig = {
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
artists?: string[];
|
|
10
|
+
frequency?: string;
|
|
11
|
+
chatTarget?: string;
|
|
12
|
+
chatChannel?: string;
|
|
13
|
+
city?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function isRecord(value: unknown): value is Record<string, any> {
|
|
17
|
+
return typeof value === "object" && value !== null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveLegacyConfig(api: any): NewLorePluginConfig | undefined {
|
|
21
|
+
const entries = api?.config?.plugins?.entries;
|
|
22
|
+
if (!isRecord(entries)) return undefined;
|
|
23
|
+
for (const pluginId of LEGACY_PLUGIN_IDS) {
|
|
24
|
+
const legacyConfig = entries[pluginId]?.config;
|
|
25
|
+
if (isRecord(legacyConfig) && Object.keys(legacyConfig).length > 0) {
|
|
26
|
+
return legacyConfig as NewLorePluginConfig;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ensurePluginEntry(api: any, pluginId: string): Record<string, any> {
|
|
33
|
+
if (!isRecord(api.config)) api.config = {};
|
|
34
|
+
if (!isRecord(api.config.plugins)) api.config.plugins = {};
|
|
35
|
+
if (!isRecord(api.config.plugins.entries)) api.config.plugins.entries = {};
|
|
36
|
+
if (!isRecord(api.config.plugins.entries[pluginId])) {
|
|
37
|
+
api.config.plugins.entries[pluginId] = {};
|
|
38
|
+
}
|
|
39
|
+
return api.config.plugins.entries[pluginId];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getPluginId(api: any): string {
|
|
43
|
+
if (typeof api?.id === "string" && api.id.trim()) return api.id.trim();
|
|
44
|
+
return LEGACY_PLUGIN_IDS[0];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function getPluginConfig(api: any): NewLorePluginConfig {
|
|
48
|
+
if (isRecord(api?.pluginConfig) && Object.keys(api.pluginConfig).length > 0) {
|
|
49
|
+
return api.pluginConfig as NewLorePluginConfig;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pluginId = getPluginId(api);
|
|
53
|
+
const currentConfig = api?.config?.plugins?.entries?.[pluginId]?.config;
|
|
54
|
+
if (isRecord(currentConfig) && Object.keys(currentConfig).length > 0) {
|
|
55
|
+
return currentConfig as NewLorePluginConfig;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return resolveLegacyConfig(api) ?? {};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function persistPluginConfig(
|
|
62
|
+
api: any,
|
|
63
|
+
updates: Record<string, any>,
|
|
64
|
+
): NewLorePluginConfig {
|
|
65
|
+
const pluginId = getPluginId(api);
|
|
66
|
+
const nextConfig = {
|
|
67
|
+
...getPluginConfig(api),
|
|
68
|
+
...updates,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
ensurePluginEntry(api, pluginId).config = nextConfig;
|
|
72
|
+
api.pluginConfig = nextConfig;
|
|
73
|
+
|
|
74
|
+
// OpenClaw does not currently expose a config.save() helper to plugins.
|
|
75
|
+
try {
|
|
76
|
+
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
77
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
78
|
+
const disk = JSON.parse(raw);
|
|
79
|
+
if (!isRecord(disk.plugins)) disk.plugins = {};
|
|
80
|
+
if (!isRecord(disk.plugins.entries)) disk.plugins.entries = {};
|
|
81
|
+
if (!isRecord(disk.plugins.entries[pluginId])) disk.plugins.entries[pluginId] = {};
|
|
82
|
+
disk.plugins.entries[pluginId].config = nextConfig;
|
|
83
|
+
writeFileSync(configPath, JSON.stringify(disk, null, 2) + "\n", "utf-8");
|
|
84
|
+
} catch (err: any) {
|
|
85
|
+
api.logger?.warn?.(`NewLore: failed to persist config: ${err.message}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return nextConfig;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function migrateLegacyPluginConfig(api: any): boolean {
|
|
92
|
+
const pluginId = getPluginId(api);
|
|
93
|
+
if (LEGACY_PLUGIN_IDS.includes(pluginId)) return false;
|
|
94
|
+
|
|
95
|
+
const currentConfig = api?.config?.plugins?.entries?.[pluginId]?.config;
|
|
96
|
+
if (isRecord(currentConfig) && Object.keys(currentConfig).length > 0) {
|
|
97
|
+
api.pluginConfig = currentConfig;
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const legacyConfig = resolveLegacyConfig(api);
|
|
102
|
+
if (!legacyConfig) return false;
|
|
103
|
+
|
|
104
|
+
persistPluginConfig(api, legacyConfig);
|
|
105
|
+
return true;
|
|
106
|
+
}
|
package/src/poll-service.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { execFile } from "child_process";
|
|
7
7
|
import * as twindex from "./client.js";
|
|
8
|
-
import {
|
|
8
|
+
import { getPluginConfig, getPluginId } from "./plugin-config.js";
|
|
9
9
|
|
|
10
10
|
const FREQUENCY_MS: Record<string, number> = {
|
|
11
11
|
realtime: 20 * 1000, // 20 sec (demo speed)
|
|
@@ -13,42 +13,25 @@ const FREQUENCY_MS: Record<string, number> = {
|
|
|
13
13
|
daily: 24 * 60 * 60 * 1000, // 24 hours
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
-
const INITIAL_POLL_DELAY_MS = 15 * 1000;
|
|
17
|
-
|
|
18
16
|
function sendMessage(
|
|
19
17
|
message: string,
|
|
20
18
|
channel: string,
|
|
21
19
|
target: string,
|
|
22
|
-
): Promise<
|
|
20
|
+
): Promise<boolean> {
|
|
23
21
|
return new Promise((resolve) => {
|
|
24
22
|
execFile(
|
|
25
23
|
"openclaw",
|
|
26
24
|
["message", "send", "--channel", channel, "--target", target, "-m", message],
|
|
27
|
-
{ timeout:
|
|
28
|
-
(err
|
|
29
|
-
if (!err) {
|
|
30
|
-
resolve({ ok: true });
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const details = [err.message, stderr?.trim(), stdout?.trim()]
|
|
35
|
-
.filter(Boolean)
|
|
36
|
-
.join(" | ");
|
|
37
|
-
resolve({ ok: false, reason: details || "unknown delivery error" });
|
|
38
|
-
},
|
|
25
|
+
{ timeout: 30_000 },
|
|
26
|
+
(err) => resolve(!err),
|
|
39
27
|
);
|
|
40
28
|
});
|
|
41
29
|
}
|
|
42
30
|
|
|
43
31
|
export function createNotificationService(api: any) {
|
|
44
32
|
let timer: ReturnType<typeof setInterval> | null = null;
|
|
45
|
-
let startupPoll: ReturnType<typeof setTimeout> | null = null;
|
|
46
|
-
let running = false;
|
|
47
33
|
let polling = false; // guard against overlapping polls
|
|
48
|
-
|
|
49
|
-
function cfg(): Record<string, any> {
|
|
50
|
-
return mergedPluginConfig(api);
|
|
51
|
-
}
|
|
34
|
+
const cfg = () => getPluginConfig(api);
|
|
52
35
|
|
|
53
36
|
function getApiKey(): string | undefined {
|
|
54
37
|
return cfg().apiKey;
|
|
@@ -156,17 +139,15 @@ export function createNotificationService(api: any) {
|
|
|
156
139
|
message += `\n\nWant to see how it looks on you? Send me a selfie and I'll show you.`;
|
|
157
140
|
}
|
|
158
141
|
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
142
|
+
const delivered = await sendMessage(message, channel, target);
|
|
143
|
+
if (delivered) {
|
|
161
144
|
try {
|
|
162
145
|
await twindex.markRead(apiKey, [notif.id]);
|
|
163
146
|
} catch {
|
|
164
147
|
// Non-fatal — delivered but read status will catch up
|
|
165
148
|
}
|
|
166
149
|
} else {
|
|
167
|
-
logger?.warn?.(
|
|
168
|
-
`Twindex: message delivery failed, will retry next poll (${delivery.reason})`,
|
|
169
|
-
);
|
|
150
|
+
logger?.warn?.("Twindex: message delivery failed, will retry next poll");
|
|
170
151
|
break; // Don't pile up failed deliveries
|
|
171
152
|
}
|
|
172
153
|
}
|
|
@@ -178,7 +159,7 @@ export function createNotificationService(api: any) {
|
|
|
178
159
|
}
|
|
179
160
|
|
|
180
161
|
return {
|
|
181
|
-
id:
|
|
162
|
+
id: `${getPluginId(api)}-notifications`,
|
|
182
163
|
|
|
183
164
|
start() {
|
|
184
165
|
// Clear any stale timer from a previous gateway reload
|
|
@@ -186,10 +167,6 @@ export function createNotificationService(api: any) {
|
|
|
186
167
|
clearInterval(timer);
|
|
187
168
|
timer = null;
|
|
188
169
|
}
|
|
189
|
-
if (startupPoll) {
|
|
190
|
-
clearTimeout(startupPoll);
|
|
191
|
-
startupPoll = null;
|
|
192
|
-
}
|
|
193
170
|
|
|
194
171
|
const apiKey = getApiKey();
|
|
195
172
|
if (!apiKey) {
|
|
@@ -197,31 +174,21 @@ export function createNotificationService(api: any) {
|
|
|
197
174
|
return;
|
|
198
175
|
}
|
|
199
176
|
|
|
200
|
-
running = true;
|
|
201
177
|
const freq = getFrequency();
|
|
202
178
|
const intervalMs = FREQUENCY_MS[freq] ?? FREQUENCY_MS.periodic;
|
|
203
179
|
|
|
180
|
+
// Immediate first poll
|
|
181
|
+
poll();
|
|
182
|
+
|
|
204
183
|
timer = setInterval(poll, intervalMs);
|
|
205
|
-
|
|
206
|
-
startupPoll = setTimeout(() => {
|
|
207
|
-
startupPoll = null;
|
|
208
|
-
poll();
|
|
209
|
-
}, initialDelay);
|
|
210
|
-
logger?.info?.(
|
|
211
|
-
`Twindex: poll service started (${freq}, every ${intervalMs / 1000}s, initial delay ${initialDelay / 1000}s)`,
|
|
212
|
-
);
|
|
184
|
+
logger?.info?.(`Twindex: poll service started (${freq}, every ${intervalMs / 1000}s)`);
|
|
213
185
|
},
|
|
214
186
|
|
|
215
187
|
stop() {
|
|
216
|
-
running = false;
|
|
217
188
|
if (timer) {
|
|
218
189
|
clearInterval(timer);
|
|
219
190
|
timer = null;
|
|
220
191
|
}
|
|
221
|
-
if (startupPoll) {
|
|
222
|
-
clearTimeout(startupPoll);
|
|
223
|
-
startupPoll = null;
|
|
224
|
-
}
|
|
225
192
|
logger?.info?.("Twindex: poll service stopped");
|
|
226
193
|
},
|
|
227
194
|
};
|
package/src/sse-service.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { execFile } from "child_process";
|
|
6
6
|
import * as twindex from "./client.js";
|
|
7
|
+
import { getPluginConfig, getPluginId } from "./plugin-config.js";
|
|
7
8
|
|
|
8
9
|
const BASE_URL = "https://newlore.ai";
|
|
9
10
|
const BACKOFF_STEPS = [1000, 2000, 5000, 10_000, 30_000, 60_000];
|
|
@@ -33,9 +34,10 @@ export function createNotificationService(api: any) {
|
|
|
33
34
|
let retryCount = 0;
|
|
34
35
|
let lastEventId: string | null = null;
|
|
35
36
|
let running = false;
|
|
37
|
+
const cfg = () => getPluginConfig(api);
|
|
36
38
|
|
|
37
39
|
function getApiKey(): string | undefined {
|
|
38
|
-
return
|
|
40
|
+
return cfg().apiKey;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
const logger = api.logger;
|
|
@@ -146,7 +148,7 @@ export function createNotificationService(api: any) {
|
|
|
146
148
|
}
|
|
147
149
|
|
|
148
150
|
return {
|
|
149
|
-
id:
|
|
151
|
+
id: `${getPluginId(api)}-notifications`,
|
|
150
152
|
|
|
151
153
|
start() {
|
|
152
154
|
if (running) return;
|
package/src/config.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
3
|
-
import { join } from "path";
|
|
4
|
-
|
|
5
|
-
type PluginConfig = Record<string, any>;
|
|
6
|
-
|
|
7
|
-
export function readDiskPluginConfig(): PluginConfig {
|
|
8
|
-
try {
|
|
9
|
-
const configPath = join(homedir(), ".openclaw", "openclaw.json");
|
|
10
|
-
if (!existsSync(configPath)) return {};
|
|
11
|
-
const raw = readFileSync(configPath, "utf-8");
|
|
12
|
-
const parsed = JSON.parse(raw);
|
|
13
|
-
return parsed?.plugins?.entries?.twindex?.config ?? {};
|
|
14
|
-
} catch {
|
|
15
|
-
return {};
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function mergedPluginConfig(api: any): PluginConfig {
|
|
20
|
-
const disk = readDiskPluginConfig();
|
|
21
|
-
const memory = api.config?.plugins?.entries?.twindex?.config ?? {};
|
|
22
|
-
return { ...disk, ...memory };
|
|
23
|
-
}
|