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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/index.ts +30 -10
- package/src/plugin-config.ts +164 -2
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "0.8.20260412",
|
|
6
6
|
"skills": ["./skills"],
|
|
7
7
|
"configSchema": {
|
|
8
8
|
"type": "object",
|
package/package.json
CHANGED
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
|
|
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
|
-
|
|
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({
|
package/src/plugin-config.ts
CHANGED
|
@@ -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 =
|
|
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
|
+
}
|