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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/index.ts +85 -35
- 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);
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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");
|
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
|
+
}
|