twindex-openclaw-plugin 0.8.20260410 → 0.8.20260412

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.
@@ -2,7 +2,7 @@
2
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.20260410",
5
+ "version": "0.8.20260412",
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.20260410",
3
+ "version": "0.8.20260412",
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
@@ -4,9 +4,12 @@ import { homedir } from "os";
4
4
  import * as twindex from "./client.js";
5
5
  import { createNotificationService } from "./poll-service.js";
6
6
  import {
7
+ claimBootstrapRegistration,
8
+ completeBootstrapRegistration,
7
9
  getPluginConfig,
8
10
  migrateLegacyPluginConfig,
9
11
  persistPluginConfig,
12
+ waitForBootstrapRegistration,
10
13
  } from "./plugin-config.js";
11
14
 
12
15
  let bootstrapping = false;
@@ -23,6 +26,31 @@ export default function register(api: any) {
23
26
  api.logger?.info?.("NewLore: config persisted to disk");
24
27
  }
25
28
 
29
+ async function ensureApiKey(): Promise<string> {
30
+ const current = cfg();
31
+ if (current.apiKey) return current.apiKey;
32
+
33
+ const claim = claimBootstrapRegistration(api);
34
+ if (!claim.claimed) {
35
+ const resolved = await waitForBootstrapRegistration(api);
36
+ if (resolved.apiKey) return resolved.apiKey;
37
+ throw new Error("registration already in progress");
38
+ }
39
+
40
+ try {
41
+ const agentId =
42
+ api.agentId ??
43
+ api.config?.agentId ??
44
+ `openclaw-${crypto.randomUUID()}`;
45
+ const reg = await twindex.register(agentId);
46
+ completeBootstrapRegistration(api, reg.api_key);
47
+ return reg.api_key;
48
+ } catch (err: any) {
49
+ completeBootstrapRegistration(api, undefined, err?.message ?? String(err));
50
+ throw err;
51
+ }
52
+ }
53
+
26
54
  // ── Poll-based notification service ──────────────────────────────
27
55
  const pollService = createNotificationService(api);
28
56
  api.registerService?.(pollService);
@@ -53,34 +81,64 @@ export default function register(api: any) {
53
81
  return "telegram";
54
82
  }
55
83
 
84
+ function isGatewayRuntime(): boolean {
85
+ const [command] = process.argv.slice(2);
86
+ return command === "gateway";
87
+ }
88
+
89
+ async function ensureArtistSubscriptions(
90
+ apiKey: string,
91
+ artists: string[],
92
+ ): Promise<string[]> {
93
+ let existing = new Set<string>();
94
+ try {
95
+ const subscriptions = await twindex.listSubscriptions(apiKey);
96
+ existing = new Set(subscriptions.map((subscription) => subscription.brand));
97
+ } catch (err: any) {
98
+ api.logger?.warn?.(
99
+ `NewLore: failed to list existing subscriptions: ${err.message}`,
100
+ );
101
+ }
102
+
103
+ const results: string[] = [];
104
+ for (const artist of artists) {
105
+ if (existing.has(artist)) {
106
+ api.logger?.info?.(`NewLore: already subscribed to ${artist}`);
107
+ results.push(`Already subscribed to ${artist}`);
108
+ continue;
109
+ }
110
+
111
+ try {
112
+ await twindex.subscribe(apiKey, artist);
113
+ api.logger?.info?.(`NewLore: auto-subscribed to ${artist}`);
114
+ results.push(`Subscribed to ${artist}`);
115
+ } catch (err: any) {
116
+ api.logger?.warn?.(
117
+ `NewLore: failed to subscribe to ${artist}: ${err.message}`,
118
+ );
119
+ results.push(`${artist}: ${err.message}`);
120
+ }
121
+ }
122
+
123
+ return results;
124
+ }
125
+
56
126
  // ── Auto-bootstrap: register + subscribe ────────────────────────
57
127
 
58
128
  (async () => {
129
+ if (!isGatewayRuntime()) {
130
+ api.logger?.info?.("NewLore: skipping auto-bootstrap outside gateway runtime");
131
+ return;
132
+ }
133
+
59
134
  if (bootstrapping) return;
60
135
  bootstrapping = true;
61
136
  try {
62
137
  const config = cfg();
63
138
  if (config.artists?.length > 0 && !config.apiKey) {
64
- const agentId =
65
- api.agentId ?? api.config?.agentId ?? `openclaw-${crypto.randomUUID()}`;
66
- const reg = await twindex.register(agentId);
67
- const apiKey = reg.api_key;
68
-
69
- for (const artist of config.artists) {
70
- try {
71
- await twindex.subscribe(apiKey, artist);
72
- api.logger?.info?.(`NewLore: auto-subscribed to ${artist}`);
73
- } catch (err: any) {
74
- api.logger?.warn?.(
75
- `NewLore: failed to subscribe to ${artist}: ${err.message}`,
76
- );
77
- }
78
- }
79
-
80
- // Auto-detect chatTarget from channel config if not explicitly set
139
+ const frequency = config.frequency ?? "periodic";
81
140
  const updates: Record<string, any> = {
82
- apiKey,
83
- frequency: config.frequency ?? "periodic",
141
+ frequency,
84
142
  };
85
143
  const chatTarget = inferChatTarget();
86
144
  if (chatTarget) {
@@ -89,7 +147,12 @@ export default function register(api: any) {
89
147
  api.logger?.info?.(`NewLore: auto-detected chatTarget=${chatTarget}`);
90
148
  }
91
149
 
92
- persistConfig(updates);
150
+ const apiKey = await ensureApiKey();
151
+ persistConfig({
152
+ apiKey,
153
+ ...updates,
154
+ });
155
+ await ensureArtistSubscriptions(apiKey, config.artists);
93
156
 
94
157
  // Set city if configured
95
158
  if (config.city) {
@@ -192,12 +255,7 @@ export default function register(api: any) {
192
255
  }
193
256
 
194
257
  if (!apiKey) {
195
- const agentId =
196
- api.agentId ??
197
- api.config?.agentId ??
198
- `openclaw-${crypto.randomUUID()}`;
199
- const reg = await twindex.register(agentId);
200
- apiKey = reg.api_key;
258
+ apiKey = await ensureApiKey();
201
259
  }
202
260
 
203
261
  persistConfig({
@@ -217,15 +275,7 @@ export default function register(api: any) {
217
275
  }
218
276
  }
219
277
 
220
- const results: string[] = [];
221
- for (const artist of params.artists) {
222
- try {
223
- await twindex.subscribe(apiKey, artist);
224
- results.push(`Subscribed to ${artist}`);
225
- } catch (err: any) {
226
- results.push(`${artist}: ${err.message}`);
227
- }
228
- }
278
+ const results = await ensureArtistSubscriptions(apiKey, params.artists);
229
279
 
230
280
  pollService.start();
231
281
  results.push("Notification polling enabled");
@@ -1,8 +1,9 @@
1
- import { readFileSync, writeFileSync } from "fs";
1
+ import { closeSync, openSync, readFileSync, writeFileSync } from "fs";
2
2
  import { homedir } from "os";
3
3
  import { join } from "path";
4
4
 
5
5
  const LEGACY_PLUGIN_IDS = ["twindex"];
6
+ const BOOTSTRAP_WINDOW_MS = 90_000;
6
7
 
7
8
  export type NewLorePluginConfig = {
8
9
  apiKey?: string;
@@ -13,6 +14,15 @@ export type NewLorePluginConfig = {
13
14
  city?: string;
14
15
  };
15
16
 
17
+ type BootstrapState = {
18
+ status: "in_progress" | "complete" | "failed";
19
+ apiKey?: string;
20
+ owner?: string;
21
+ startedAt?: string;
22
+ updatedAt?: string;
23
+ error?: string;
24
+ };
25
+
16
26
  function isRecord(value: unknown): value is Record<string, any> {
17
27
  return typeof value === "object" && value !== null;
18
28
  }
@@ -29,6 +39,63 @@ function resolveLegacyConfig(api: any): NewLorePluginConfig | undefined {
29
39
  return undefined;
30
40
  }
31
41
 
42
+ function getConfigPath(): string {
43
+ return join(homedir(), ".openclaw", "openclaw.json");
44
+ }
45
+
46
+ function getBootstrapStatePath(pluginId: string): string {
47
+ return join(homedir(), ".openclaw", `${pluginId}-bootstrap.json`);
48
+ }
49
+
50
+ function readJSON(path: string): any | undefined {
51
+ try {
52
+ return JSON.parse(readFileSync(path, "utf-8"));
53
+ } catch {
54
+ return undefined;
55
+ }
56
+ }
57
+
58
+ function readDiskPluginConfig(pluginId: string): NewLorePluginConfig | undefined {
59
+ const disk = readJSON(getConfigPath());
60
+ const currentConfig = disk?.plugins?.entries?.[pluginId]?.config;
61
+ if (isRecord(currentConfig) && Object.keys(currentConfig).length > 0) {
62
+ return currentConfig as NewLorePluginConfig;
63
+ }
64
+
65
+ for (const legacyPluginId of LEGACY_PLUGIN_IDS) {
66
+ const legacyConfig = disk?.plugins?.entries?.[legacyPluginId]?.config;
67
+ if (isRecord(legacyConfig) && Object.keys(legacyConfig).length > 0) {
68
+ return legacyConfig as NewLorePluginConfig;
69
+ }
70
+ }
71
+
72
+ return undefined;
73
+ }
74
+
75
+ function readBootstrapState(pluginId: string): BootstrapState | undefined {
76
+ const raw = readJSON(getBootstrapStatePath(pluginId));
77
+ if (!isRecord(raw)) return undefined;
78
+ if (raw.status !== "in_progress" && raw.status !== "complete" && raw.status !== "failed") {
79
+ return undefined;
80
+ }
81
+ return raw as BootstrapState;
82
+ }
83
+
84
+ function writeBootstrapState(pluginId: string, state: BootstrapState): void {
85
+ writeFileSync(
86
+ getBootstrapStatePath(pluginId),
87
+ JSON.stringify(state, null, 2) + "\n",
88
+ "utf-8",
89
+ );
90
+ }
91
+
92
+ function isFreshBootstrap(state?: BootstrapState): boolean {
93
+ if (!state || state.status !== "in_progress") return false;
94
+ const startedAt = Date.parse(state.updatedAt ?? state.startedAt ?? "");
95
+ if (Number.isNaN(startedAt)) return false;
96
+ return Date.now() - startedAt < BOOTSTRAP_WINDOW_MS;
97
+ }
98
+
32
99
  function ensurePluginEntry(api: any, pluginId: string): Record<string, any> {
33
100
  if (!isRecord(api.config)) api.config = {};
34
101
  if (!isRecord(api.config.plugins)) api.config.plugins = {};
@@ -71,7 +138,7 @@ function persistConfigToDisk(
71
138
  pluginId: string,
72
139
  nextConfig?: NewLorePluginConfig,
73
140
  ): boolean {
74
- const configPath = join(homedir(), ".openclaw", "openclaw.json");
141
+ const configPath = getConfigPath();
75
142
  const raw = readFileSync(configPath, "utf-8");
76
143
  const disk = JSON.parse(raw);
77
144
  let changed = false;
@@ -109,6 +176,19 @@ export function getPluginConfig(api: any): NewLorePluginConfig {
109
176
  return currentConfig as NewLorePluginConfig;
110
177
  }
111
178
 
179
+ const diskConfig = readDiskPluginConfig(pluginId);
180
+ if (diskConfig) {
181
+ api.pluginConfig = diskConfig;
182
+ return diskConfig;
183
+ }
184
+
185
+ const bootstrapState = readBootstrapState(pluginId);
186
+ if (bootstrapState?.apiKey) {
187
+ const bootstrapConfig = { apiKey: bootstrapState.apiKey };
188
+ api.pluginConfig = bootstrapConfig;
189
+ return bootstrapConfig;
190
+ }
191
+
112
192
  return resolveLegacyConfig(api) ?? {};
113
193
  }
114
194
 
@@ -157,3 +237,85 @@ export function migrateLegacyPluginConfig(api: any): boolean {
157
237
  persistPluginConfig(api, legacyConfig);
158
238
  return true;
159
239
  }
240
+
241
+ export function claimBootstrapRegistration(api: any): {
242
+ claimed: boolean;
243
+ config: NewLorePluginConfig;
244
+ } {
245
+ const pluginId = getPluginId(api);
246
+ const config = getPluginConfig(api);
247
+ if (config.apiKey) return { claimed: false, config };
248
+
249
+ const state = readBootstrapState(pluginId);
250
+ if (state?.apiKey) {
251
+ const nextConfig = persistPluginConfig(api, { apiKey: state.apiKey });
252
+ return { claimed: false, config: nextConfig };
253
+ }
254
+ if (isFreshBootstrap(state)) {
255
+ return { claimed: false, config };
256
+ }
257
+
258
+ const statePath = getBootstrapStatePath(pluginId);
259
+ try {
260
+ const fd = openSync(statePath, "wx");
261
+ closeSync(fd);
262
+ } catch (err: any) {
263
+ if (err?.code === "EEXIST") {
264
+ const existing = readBootstrapState(pluginId);
265
+ if (existing?.apiKey) {
266
+ const nextConfig = persistPluginConfig(api, { apiKey: existing.apiKey });
267
+ return { claimed: false, config: nextConfig };
268
+ }
269
+ if (isFreshBootstrap(existing)) {
270
+ return { claimed: false, config };
271
+ }
272
+ } else {
273
+ throw err;
274
+ }
275
+ }
276
+
277
+ writeBootstrapState(pluginId, {
278
+ status: "in_progress",
279
+ owner: `${process.pid}:${Date.now()}`,
280
+ startedAt: new Date().toISOString(),
281
+ updatedAt: new Date().toISOString(),
282
+ });
283
+
284
+ return { claimed: true, config };
285
+ }
286
+
287
+ export function completeBootstrapRegistration(
288
+ api: any,
289
+ apiKey?: string,
290
+ error?: string,
291
+ ): void {
292
+ const pluginId = getPluginId(api);
293
+ writeBootstrapState(pluginId, {
294
+ status: apiKey ? "complete" : "failed",
295
+ apiKey,
296
+ error,
297
+ updatedAt: new Date().toISOString(),
298
+ });
299
+ }
300
+
301
+ export async function waitForBootstrapRegistration(
302
+ api: any,
303
+ timeoutMs = 20_000,
304
+ ): Promise<NewLorePluginConfig> {
305
+ const pluginId = getPluginId(api);
306
+ const deadline = Date.now() + timeoutMs;
307
+
308
+ while (Date.now() < deadline) {
309
+ const config = getPluginConfig(api);
310
+ if (config.apiKey) return config;
311
+
312
+ const state = readBootstrapState(pluginId);
313
+ if (state?.apiKey) {
314
+ return persistPluginConfig(api, { apiKey: state.apiKey });
315
+ }
316
+
317
+ await new Promise((resolve) => setTimeout(resolve, 250));
318
+ }
319
+
320
+ return getPluginConfig(api);
321
+ }