twindex-openclaw-plugin 0.8.5 → 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 -47
- 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,43 +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
|
-
const MESSAGE_SEND_TIMEOUT_MS = 30 * 1000;
|
|
18
|
-
|
|
19
16
|
function sendMessage(
|
|
20
17
|
message: string,
|
|
21
18
|
channel: string,
|
|
22
19
|
target: string,
|
|
23
|
-
): Promise<
|
|
20
|
+
): Promise<boolean> {
|
|
24
21
|
return new Promise((resolve) => {
|
|
25
22
|
execFile(
|
|
26
23
|
"openclaw",
|
|
27
24
|
["message", "send", "--channel", channel, "--target", target, "-m", message],
|
|
28
|
-
{ timeout:
|
|
29
|
-
(err
|
|
30
|
-
if (!err) {
|
|
31
|
-
resolve({ ok: true });
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const details = [err.message, stderr?.trim(), stdout?.trim()]
|
|
36
|
-
.filter(Boolean)
|
|
37
|
-
.join(" | ");
|
|
38
|
-
resolve({ ok: false, reason: details || "unknown delivery error" });
|
|
39
|
-
},
|
|
25
|
+
{ timeout: 30_000 },
|
|
26
|
+
(err) => resolve(!err),
|
|
40
27
|
);
|
|
41
28
|
});
|
|
42
29
|
}
|
|
43
30
|
|
|
44
31
|
export function createNotificationService(api: any) {
|
|
45
32
|
let timer: ReturnType<typeof setInterval> | null = null;
|
|
46
|
-
let startupPoll: ReturnType<typeof setTimeout> | null = null;
|
|
47
|
-
let running = false;
|
|
48
33
|
let polling = false; // guard against overlapping polls
|
|
49
|
-
|
|
50
|
-
function cfg(): Record<string, any> {
|
|
51
|
-
return mergedPluginConfig(api);
|
|
52
|
-
}
|
|
34
|
+
const cfg = () => getPluginConfig(api);
|
|
53
35
|
|
|
54
36
|
function getApiKey(): string | undefined {
|
|
55
37
|
return cfg().apiKey;
|
|
@@ -157,17 +139,15 @@ export function createNotificationService(api: any) {
|
|
|
157
139
|
message += `\n\nWant to see how it looks on you? Send me a selfie and I'll show you.`;
|
|
158
140
|
}
|
|
159
141
|
|
|
160
|
-
const
|
|
161
|
-
if (
|
|
142
|
+
const delivered = await sendMessage(message, channel, target);
|
|
143
|
+
if (delivered) {
|
|
162
144
|
try {
|
|
163
145
|
await twindex.markRead(apiKey, [notif.id]);
|
|
164
146
|
} catch {
|
|
165
147
|
// Non-fatal — delivered but read status will catch up
|
|
166
148
|
}
|
|
167
149
|
} else {
|
|
168
|
-
logger?.warn?.(
|
|
169
|
-
`Twindex: message delivery failed, will retry next poll (${delivery.reason})`,
|
|
170
|
-
);
|
|
150
|
+
logger?.warn?.("Twindex: message delivery failed, will retry next poll");
|
|
171
151
|
break; // Don't pile up failed deliveries
|
|
172
152
|
}
|
|
173
153
|
}
|
|
@@ -179,7 +159,7 @@ export function createNotificationService(api: any) {
|
|
|
179
159
|
}
|
|
180
160
|
|
|
181
161
|
return {
|
|
182
|
-
id:
|
|
162
|
+
id: `${getPluginId(api)}-notifications`,
|
|
183
163
|
|
|
184
164
|
start() {
|
|
185
165
|
// Clear any stale timer from a previous gateway reload
|
|
@@ -187,10 +167,6 @@ export function createNotificationService(api: any) {
|
|
|
187
167
|
clearInterval(timer);
|
|
188
168
|
timer = null;
|
|
189
169
|
}
|
|
190
|
-
if (startupPoll) {
|
|
191
|
-
clearTimeout(startupPoll);
|
|
192
|
-
startupPoll = null;
|
|
193
|
-
}
|
|
194
170
|
|
|
195
171
|
const apiKey = getApiKey();
|
|
196
172
|
if (!apiKey) {
|
|
@@ -198,31 +174,21 @@ export function createNotificationService(api: any) {
|
|
|
198
174
|
return;
|
|
199
175
|
}
|
|
200
176
|
|
|
201
|
-
running = true;
|
|
202
177
|
const freq = getFrequency();
|
|
203
178
|
const intervalMs = FREQUENCY_MS[freq] ?? FREQUENCY_MS.periodic;
|
|
204
179
|
|
|
180
|
+
// Immediate first poll
|
|
181
|
+
poll();
|
|
182
|
+
|
|
205
183
|
timer = setInterval(poll, intervalMs);
|
|
206
|
-
|
|
207
|
-
startupPoll = setTimeout(() => {
|
|
208
|
-
startupPoll = null;
|
|
209
|
-
poll();
|
|
210
|
-
}, initialDelay);
|
|
211
|
-
logger?.info?.(
|
|
212
|
-
`Twindex: poll service started (${freq}, every ${intervalMs / 1000}s, initial delay ${initialDelay / 1000}s)`,
|
|
213
|
-
);
|
|
184
|
+
logger?.info?.(`Twindex: poll service started (${freq}, every ${intervalMs / 1000}s)`);
|
|
214
185
|
},
|
|
215
186
|
|
|
216
187
|
stop() {
|
|
217
|
-
running = false;
|
|
218
188
|
if (timer) {
|
|
219
189
|
clearInterval(timer);
|
|
220
190
|
timer = null;
|
|
221
191
|
}
|
|
222
|
-
if (startupPoll) {
|
|
223
|
-
clearTimeout(startupPoll);
|
|
224
|
-
startupPoll = null;
|
|
225
|
-
}
|
|
226
192
|
logger?.info?.("Twindex: poll service stopped");
|
|
227
193
|
},
|
|
228
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
|
-
}
|