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.
@@ -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.4",
5
+ "version": "0.8.20260409",
6
6
  "skills": ["./skills"],
7
7
  "configSchema": {
8
8
  "type": "object",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "twindex-openclaw-plugin",
3
- "version": "0.8.4",
3
+ "version": "0.8.20260409",
4
4
  "description": "Music intelligence for AI agents. Tours, merch drops, releases, presales.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -1,40 +1,26 @@
1
- import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
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 { mergedPluginConfig } from "./config.js";
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 = () => mergedPluginConfig(api);
15
+ const cfg = () => getPluginConfig(api);
12
16
 
13
- function persistConfig(updates: Record<string, any>) {
14
- const nextConfig = {
15
- ...cfg(),
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
- // Write to disk — OpenClaw plugin API does not expose config.save()
25
- try {
26
- const configPath = join(homedir(), ".openclaw", "openclaw.json");
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 && config.frequency && !config.apiKey) {
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> = { apiKey };
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", "frequency", "chat_target"],
164
+ required: ["artists"],
176
165
  },
177
166
  async execute(
178
167
  _id: string,
179
- params: { artists: string[]; frequency: string; chat_target: string; chat_channel?: string; city?: string },
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: params.frequency,
198
- chatTarget: params.chat_target,
199
- chatChannel: params.chat_channel ?? "telegram",
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
+ }
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { execFile } from "child_process";
7
7
  import * as twindex from "./client.js";
8
- import { mergedPluginConfig } from "./config.js";
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<{ ok: boolean; reason?: string }> {
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: 10_000 },
28
- (err, stdout, stderr) => {
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 delivery = await sendMessage(message, channel, target);
160
- if (delivery.ok) {
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: "twindex-notifications",
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
- const initialDelay = Math.min(INITIAL_POLL_DELAY_MS, intervalMs);
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
  };
@@ -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 api.config?.plugins?.entries?.twindex?.config?.apiKey;
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: "twindex-notifications",
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
- }