twindex-openclaw-plugin 0.8.20260411 → 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.20260411",
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.20260411",
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);
@@ -119,10 +147,7 @@ export default function register(api: any) {
119
147
  api.logger?.info?.(`NewLore: auto-detected chatTarget=${chatTarget}`);
120
148
  }
121
149
 
122
- const agentId =
123
- api.agentId ?? api.config?.agentId ?? `openclaw-${crypto.randomUUID()}`;
124
- const reg = await twindex.register(agentId);
125
- const apiKey = reg.api_key;
150
+ const apiKey = await ensureApiKey();
126
151
  persistConfig({
127
152
  apiKey,
128
153
  ...updates,
@@ -230,12 +255,7 @@ export default function register(api: any) {
230
255
  }
231
256
 
232
257
  if (!apiKey) {
233
- const agentId =
234
- api.agentId ??
235
- api.config?.agentId ??
236
- `openclaw-${crypto.randomUUID()}`;
237
- const reg = await twindex.register(agentId);
238
- apiKey = reg.api_key;
258
+ apiKey = await ensureApiKey();
239
259
  }
240
260
 
241
261
  persistConfig({
@@ -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
+ }